@pixelated-tech/components 3.9.14 → 3.9.16

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.
@@ -22,6 +22,7 @@ export function getFullPixelatedConfig() {
22
22
  // If this library is installed as a package, check its dist/config as a fallback
23
23
  path.join(process.cwd(), 'node_modules', '@pixelated-tech', 'components', 'dist', 'config', filename),
24
24
  ];
25
+ // First, look for plaintext config files
25
26
  for (const configPath of paths) {
26
27
  if (fs.existsSync(configPath)) {
27
28
  try {
@@ -34,13 +35,46 @@ export function getFullPixelatedConfig() {
34
35
  }
35
36
  }
36
37
  }
38
+ // If not found, look for encrypted variants in the same locations (e.g., pixelated.config.json.enc)
39
+ const doIt = false;
40
+ if (!raw && doIt) {
41
+ for (const configPath of paths) {
42
+ const encPath = `${configPath}.enc`;
43
+ if (fs.existsSync(encPath)) {
44
+ try {
45
+ raw = fs.readFileSync(encPath, 'utf8');
46
+ source = encPath;
47
+ break;
48
+ }
49
+ catch (err) {
50
+ console.error(`Failed to read encrypted config file at ${encPath}`, err);
51
+ }
52
+ }
53
+ }
54
+ }
37
55
  if (!raw) {
38
56
  console.error('pixelated.config.json not found. Searched in src/app/config/, src/config/, and root.');
39
57
  return {};
40
58
  }
41
59
  // Handle decryption if the content is encrypted
42
60
  if (isEncrypted(raw)) {
43
- const key = process.env.PIXELATED_CONFIG_KEY;
61
+ // Allow key to come from env or a local .env.local fallback (useful for local/CI debugging)
62
+ let key = process.env.PIXELATED_CONFIG_KEY;
63
+ if (!key) {
64
+ const envPath = path.join(process.cwd(), '.env.local');
65
+ if (fs.existsSync(envPath)) {
66
+ try {
67
+ const envContent = fs.readFileSync(envPath, 'utf8');
68
+ const match = envContent.match(/^PIXELATED_CONFIG_KEY=(.*)$/m);
69
+ if (match && match[1]) {
70
+ key = match[1].trim();
71
+ }
72
+ }
73
+ catch (e) {
74
+ // ignore
75
+ }
76
+ }
77
+ }
44
78
  if (!key) {
45
79
  console.error('PIXELATED_CONFIG is encrypted but PIXELATED_CONFIG_KEY is not set in the environment.');
46
80
  return {};
@@ -1 +1 @@
1
- pxl:v1:d86199b0d8de38bec4e09834:31782f4ce4b3da8ef22b36edcd9306eb:79aec976d7f0040dda5acd6d045aa172e85b643e1a3542554b4f15ea36e1f3e919e79c43084fe68e3a0a939e92584df9e05ea4dfe27421c648eccca9c1a4db4ae34941472b19852ece1dadd1b6e8513addc9e45421f7bb96cceec2656832698ee11ae5bd5aafef1e3ce894e3ce1b5af1bdeb8dd7e2466fbb71b596e09f76b984c0ba4aaa864a3026b688c579006c11eea0e9282cf3eee42338ecfb4e0d5226f3b97caa07d72c996ebe782c8b9e352d3503f2d109a41a219d6ffada607edbf5b21c11b18384dc44116bea5c4c9cc9050ba7e5244f60a8f394b322a091d9be9a97dc6c44d1acc9c8010446ca97d3ec3bd9f9d70a92985ec62002ebbd73ba423810499167c9fc76a65049b963ab19b7fc4571e6b8ded81b9c6ccbbe360893f4da1859d0640b2dc9c6acb49d36999bc726eebb9dc205f9aec37278778b7550416a90261cb846bcede8ee8ec3f9d549fb85f51fe956895ae0170b89d39af624475a7a8dc00ccf919eba2083d3822acb13a2617003909590b8b34b2854cd249cf9b15cbd3f7be54ed35ac4cbfc247eabd402a2b47f1e1f653ff69da88427cbdf0e10522745d66993d32aec1ae42ecb1dc1a5a0075402c08886013b34d22c9d9cafb41b4380fdd85d4dc5ae23d3ee05144695d53b135677f196b85dde703441641722911a1291a30d661f0f8d06ca62f0b22faec8c67bcc4eb0cad78a9adba042fe0deddc04cde62c8fe917f7d10cc7dc14e67a8ab4e5bff4be1a65d9e9e03e1fa028236b78e0928128068e3f6179b092e4fa16bb6514773144a1319383d9d3a5798a4ea6acc3bfbd6314b467854a24446ecf9b299fc706fb8c05df1dcc00af66d985121e1fa76e3cf8e4ec54dc3a783faf597d494853da78103d41778983d98f27336a2d31c3fd0af08b4a7b19dcc4035f4f6d022e6f768eb63d8586944cd594bc2579b59f16a711c0653db7dc8b30175573603d0199dc247e587d66423aa7ac75a3eb8523a0169ec936895e74f4743ebbce39995f9e19130db4bf5abe16b0c1d021fb9226fe05ddda0612d4733574c993d1c302b15d0943a23835686a43c4978672594ea60e44eefb8150879f6513b4b9ab1bb756e8d2935f1575770e09f497a6da049a99d1fbd5a395d630746be4bc9708c0567584f0bfab807ab0b6db5a64a563f151f1162c4e23391ad95fa0aa53f46065b7b854153c10d807afb3dd2622a96c85f36456db7c59c9ecac4fd17f79eec186c59736b6c9a62f811fd9eed23e6c7cbf570476eb517d8db02685256140a25261e823b701a9ce7cbb76230c4b73b3f8b98fdfd3f1f61f0755f8c64f0bbf56bac8101df424d5eb243adfda361601cc20559c53fa9f847f92c63cf6f64cd1588aebadaf20f98b9aac4aede0b01b4fd307727def2f866afae6f282774433431c65adb1edbd8e6496668a7cb78a70c427f7a75112e4033f814500000d4f0e7e6c9b997a67e3bd172418bdf7368d22a98c6706cf5ccd6c0c2e11f932a52e341ee35d60baaaa4a989b54e615fa32a9dc4f27a0437be0cb67656d9bc7ca9f581d4a8cdf828e6028af0598c7c5bf729a9a69630ba6ff3c51b3732895cd83dc395e32b8187d926910271cf0b6da7cccb311ccee1f760aa472977f2ed9463e51a8c425cf50364423d69201fb488aa609eddf27d0101069706aeaf6a528a9df1207730877c4c71642ee2857619c325d31b04530c37ce01ec81fd79aac6049b989b11ef8ed67161e3e08151d150a4d1231e4c48ba3cb89f9162038d3ab9be90346b819b6f85d966c8f7721232be892caf74b4488c2adcd1613760ac321b5d8e92035c7cb766963fde89fd72dfada9f92c4bc1d320f950aa00b525bd7728c9606c68ace7a19728b3dc6d72625fd540f35d2f857dc91a79ddf40b211d2bdf42053601497c461043bb926f84a8bf39a541a8738279e1e838a55f1b9dc382e1db289d008e330f1d88b98d5cf0d642cf58e79703864f4e4329d8af2ba252659574909739d96dec4ea571c3ac4339e42919963f81042becaf00edef3f142b624abe2e94b0efe062aa96c27a4f59c5dc3fa78fd72b86ba105aebd1b054f3c7a1f9d59de5a731998d047f7cbffdcef6cc60ea74ee4d878e568d13ce30163940e4d1a07a17f8b8e041458c8c3ead293b1b6cc68d0c0dd0eef7466b0a92032ddda16d76d013b103d4c64ae772d8d9ec930361c594b5fc26e5bd4ba2c396df2430df6c55fdbce8b1b81ba30b66b0132cb39ae58e7dcd21309187f24f1508be732bd6858f5dab51c166191d57d627066976a5af6ed814da14460d28b55b3d00d57e3f4d06ec710f34dc9f08843c0ae50cec30f4def0a4a2177a3197696e67e0dd7fed1647fd11bea01f65ad1ca678a6e5588b3a20f1a2b773af0753d251a98b39ef1cbd211da7834b6edfe8a1e1bd9b5cfae917502f1105935274600807dfe824552f760af29265be5d176cd63128468e66f0ab51f4e796cb2500378082b33d2c55057daa5a1ddc7a2344cc4139758d299c6b126eeb56f106d7bf1b6344d6bd3390b6a9b947c3ac8efaa0a3fc4e7d1492fffe620682bc083af83c46392ffe9ea467b8f5b3cb5c330c8b91d684d3efcb0ea0f5b9d2b2ed3456ed65d557066219dba814368b5440cb7238b9c5ec09e8f68ddbdb425e6e1307abb6c199a3bd2b35ce86a386872cf5a82bfc2d268dd9b2edb4815277b3a08c14f4a15378580c6dac2ca5c391a9b123fbacdbb2797e04cd28bcf95b414a96496b74355dbb0809c186eb44fcf39f7e0f005dca6f7ab455d1cb41c79413c0a06b837cb3e451843bbdf3062a62b72b7fa42f7ce54503e8ef97e27e717d95435b1e2d9ecd9cffb1a1d293c7fd681af152fd418cb5838de996ee5324fab602cd7e597ebbce21e9b9b301db607d83264e63c4166d37831f9c25573892c8cee8aff44783355819c5a0c6aa1620c99504568166922ffe10abb296557b6fc21e5aad84255f903c9b9e69f496f5741699537b4e0efff604b3e01b028d89474ef72f2c19b35045de8995fd4b2303d7f7eeea99d8a45afa728ee887f9fe7f4d9a87849aff48786fedf479316f62af025bce0bbe21bf66726cec708a19ec2b14aad33936c87af30654a2abc8e26aea4dee04d115c24d9b75ec585d1137d271ca542e32092def1b66747b6bdc3a1ffa9e4a4aaa0a2eccf394744ac3b3e96bca61aee0cac0aa635a500f44ce52237a5c06b2ac1d06e4bf52c7cc985bf24d906fe5092d63e26b07d1dcdb8743ff8ec16c7519883f989e614f9356403dc3ee16c71bc2c4d9b3e6d5d75442a8fdf3f7315b58c4a6e82f7635a70e1ad55255c778403b2fcb98aeb1c7855bb2fd6c826eb27cbdad298238ce03c20d9f15afd7a4a47c978c656785a2c4b76f96893e7edced8c4e1935213c2a1c6648ef654c080ee7db5780d73940545abe459de767ea9ccc31e12a2cdedfe9a8095c33a73109df8ae3fd70a7cbe32fafafb7b82d0e52a20abc61d6158feeec435c6c6cf9ad56d1a4310d8a981191ab1fe1f57eb2f4debabaa1d81f95a6dc249e62a4c5abf475acef0a7d833d3e4852ae05f3905ecfec094f919032351fc0469c06b4c2a14a55f565072f1642b1a862f218f9300476803d8204a89d24cce0a82974143df5ec56ba000e320aababded5d2de74a3de28448363ee4f3f4c91b2dd5363e6f5052986d567e7f764d10dc96faba9cb3eadb286e2c92effb7f0f83fad4756c35947f698193df4d78f6ae8976dfea865b87e5374429eac7dce6348859a1815d0ab8b04588b4b93c32cb5748a2bbb29c953a40a43085b3848a35d43f7e1f3262ce395fc7682f3df8f7393997114a7a2174808caa656fd1b80075fc3b3c48d2985e7e54fccf869536f63592bc716614da37a8dcfde0a7db60f70bb0498e9c3dbede6fc849e4ea856b44c64535174b196dbcbb4f61b2bfe6019bb69c274c60d6b42ab79eb052dfa0e0c92de96115d9504fe948db6407368228c27566b087f640ef84de4cdf42000c1e9ba441b93f8d90dc2ad8cd39c20061f85830a30c6a783c8f73553e2002b72c6353881df1869af27f4c9754528c2ecebd391cc07b97cb17d47e89e9588a535f12c12debbb1a4f7521c82182e61a655ae03a68b55dfbccb09903812ba86a24c23fcb52dfdf28ab66a9761023acaa851f269fc0234af57c3f6692a960609ec577253844afc87a11360dc40daaf7e0736e85098e9e82b45fbad94f1ecb5767a8f0bee9b26fe7c5beb51f1f107b6adafd817c51f92150a7012968128bb3eb1916b92ee4cef5e27bd022808910dba0373be4f6a16fe92a5cc5d0b970d44532774c1107623ff5984cfacd5fc148cfce3c25ea9fc5cff7040adc21678cd074b8bf759403a5c28a60b1964f35bab7b83e0d9c77895254d918c2c18bd122115003b5d28b526229dcc108935e0d167252da8e5403680c6f6774af057d9e5d75d4172535d155474d468be968626cb021231d99f6dfaf5f97b7342667d4fa3539fe8283b9593e440e2c793fc735cc0efd9c53b32bed751bbcb929807fb805ef0ee054f30248655aa583b4b6910f5adab2404116fb985648c7d7ffdbb6d32748d2a5090eb95690945e08180ffcb965dd8f6a155ef1c83f3e353a083d782e2117f01545722ebe611f1eb2279694307726c221e7dc8059ab6085dc41f30cd24bac250d700fa207b3e28e4dc3199279cd026d968e051337a2acb0ce35de91069d9f436d397ed1b2cb6bb813bbc8c5dca8008b0cc76686d59fce2de03787a092e0ce6e46133fbd59ab6eedb3465ba376064bc09e96f7cee3815d421b0aa1e3575038a90676ce25adf2a4aa27cb75bde5ef8f624185df691c838b65cc328315beebfdbc2f8aacf113f6c6514d61d011b9d5c9d7a4a8c8b1081b5802bea140605d38a5f5c09fcd83cc64f8754d6b2475316d251edb7810f2f03fa6d14551356ebc77270aee83dd7a2e4f834e2231595b0081e1e03d5b7cb5018c7345b7416715b63e647265b5fb6189df74d0bb07c3415a8064ecd1376d543d1f477f559a59970bc8bb712a2fef1b89c45b5ecf50d31d78f073bbd16cfda486584dd357e8e984a0fb42f14b935dcd0cb50d106b5b7cd8677e582eb8ec28030181e1284f7388de2cc3d776e65a0dcaf340db996d0f4a05ec3622d18a35f4fa764e9b2f96b23f1b6dcb0
1
+ pxl:v1:1cf1822b5a08925a84ce42d5:4978c2e76935bd83c257792b8af5ff03:daf4d881dfb135ee031bde2792f3ce62a76e91c8c84c831ada3c6295607b07e78052e37637cfc792ff87d4e9b24d4fa1210319a7a238a92a5735d1bace6f153e4e844eec10353010b65cadd2eb5c926619744b341acc8e879bc926f2d60172b682e7eae401198175d2c0b39dfcfd1c6bf6aa95eaea1a131cdda6559a62191ee085a9cf3b3c35495fc29f6697cef26ca9a32ed80145875465a554db7035d1fdba56963074ee5c0e0069b963c9b314f9ed955111c93cb5e2594ae09e44a9c4a8e9686b01909c32dbf997e195ddaa142d554cc3d77fbf2a7bbdad9dfcbcc4e0a2af4efd4855b6619c06d1c5aac21e6bd7ebf13ae4a5e7a847dee7af073087225d4e34899177bde3d5b52d136505c6cf7b109e7e085aa8f96359537b33beea1f222c9d6744d91bd4ad72b3ea6a9477b3b8c7480c4128813cde65a8fc251ff2ea7268819dd45682afb67be2dc7b99c5ffd8d2f566c465fd3aa30e64378a2d5866bc980a247594fb8e494870cd8bd9f5738637180db9e582a3ba4ccf48c55403dedcc02cb2596ba2290f5753f38503225c89fcf2346147ed85355bcb940d0131eb5ca377a1efddff4772faef613752dcb3b1b5f4d06d03ded0ac1a2ae2a78035e98fcff0a38e0df2249028be3a39b86f63616cc5560efd3fc80517780e5524175d06e07cbf0631e0570c929a56fc3b7b8ac85807ae7f2d83673ff11e70273bd9d124853f3c97658819391d387377d40a11407e0a517a6cf40b1e401d88428a86620829062341a38921ee0adf03e182c573e542200be18d40045e8f229e85607936ed9bbebd0f164d1b78a5b732d4befc7aa51f4cf21775e31170022d84cb9d4ef51b8474f5a40117769853d125a69d23d418f419ad7dc21a740caa006a2ba50390973d7d62eab5d979deb12531286b92f35a1c48e479e7222637961dcadc7bdbcf4608199d01e3d13f6a682bf82896edddaefc8aaf181ad28fa514fea84fabb7469d48026f9642258836097ba8577d218b4aebaaa4bd64eac5e6e342980d2e486e482f6986ad39cbc5769a5847ef9cdf3565d2742030a78db96f27f5e40169d1a6b565f972e8772641955e9160a0413bb38ab1b99305dcd037a754115e516ed66c77f011532fcfa187d8e6400459afe8d4aef99a636a2e1a4eb02cd7fb3445198c2963368c3b3091046e768d5bce238a335e66ae300cb341e15dfbd32dde80e0d7ccd54e42776ba0d5bab7fd7756a77fd8cc1bf18d11e5e973a0808d57bda74b61a884303460b1069001731bc5bc15d0513cb2e3d32baffbdce05b56979f13dd9d9fb3d7a0dc2318f5759d299d3224b54c13043e3805b2561732616c8f0d65d0e8ef703ccf0a5ce3d9f6ca1c9451e534f281d48bd94f9f71cd1aa8c752cc2415fe1b311d91babd1dbdc0aa5f879e2aeb850fdcb9079665fa221f70144edc773640de4b50cd3d9244a1860a4241c24531f6c17cc475db50c1e42f80bcd3554716c19839c91d87be5c6431df3ce2af2a155bafe73e10d69e3c1c1fce5afe2b68e0f5a1d61baaa872e4afd701acb2e2a51ffbe44efae010e947b60f974dd32403b39133b29e63581d72b6b6585322e8e2ec6a3865a27bb360f3b61587188606b66dcaa3591ec6cd29b240421a469e966d9347bffbe3789c26ace9b7a8f4b701da7f33c21c63ff70f077c6d66b69282aa2800d29734353982d2b584dafd8dd62fad6c6e6ccc69ec5eca171a290f0cb2010b953683a7764eac31d1d5ff77eda7ce5a12895ad046acb5f77a89b1aea2574039b0bd93cbed60fa350cbfd3c0dacf035dd07b97ad4a9afbca984fc0e92569b5c7894b4c9fe5473a10debc8e4d8be933fab114ada62f4ac8fc09cab343bb883b59f7d40b03e204ba1583487addab5b22b0f81a6f3b99c7d7ec06a426b621c30c5c93c86a5a592e6b886dd1edb28c1a21a3f7f483e0b8ad39e674c13c85df960f761b5fb793415b315c1001a0df06dac8b4183bd00360d3fd94624cdb6c0a0d5f3b4c0a6f6c3c1a87ca55a17288b86bf50a80a960e9f69de45d54a8a85eef935adc2d4563be491b4357c52a82d3dc7972c8253195f062cf4ada45c790cbb8f9eead86fd6bcd400ce1a829ae83ff124f58ea1bbbe32f34ef2b6954e870f9fa3f8a9d2bc1e305ccd51d1be958ec039b4004242f08562214f8bb2e668913b3d643ddacbd02e9535a16844861046141174629e969e992c1e53e58923b0d9f306b6c622f009b31f90510d78f9bfa319ea547d83bb5aeef79f67b2202db3d1e88c83822ee3e14f953d4908024e83b08367fd4468288d7b2e843f00bb5042d45ed9a9183721a6ac806e587178c220cf5c46fb40cd5b1f9de4106fdd9d4649f56b3092075b8e10e98c2cf743e0350e55e1d50d42191d89b2e5354f8c92d3962a9d93ae6f3222d346c90269c14a560ef0f723dba47049b49a47a23b721dc9ad48d7175bd587c9ab35159d532f529113f27481c76958147c576905dec0aabf1092867bafab53e65aca7d480672f52022bbf4eb28f48ae993d80c0934295f11753c71cf751e21cb619fe0657be41b2bb06e22f7c9c55f2eaf25eb99ae47091b4239e01d7fe6d42d5e4f6de01bf3a3bc5e79b4f0862be0fedba3ffb19b0417e326a750b75ba04804d1a7799dbb2ec7fce5d58805019f4b6ed287ae5f68ec348a607624e5f2ff213f45a5bdb3ecfe10a3aaf47044b1baa1160f21e01050fe2c8a9840e07288458f70550476abbdb53ecd4f2080f002265b5d94033ee097cc2bb85cc2d4a152b93992329817dd0e4ecff51972507090a25bd7689087cdf941d6fdb7ef6622859a90032b316ea5d498cd9048eeebfa8936ba3d114f60a4658069ef6cdc41e10ba951766cd8a5672f36f34f0f2d5de67f8a5c0ab55519e0ab542f0d34c7a96c4f6c703b1dc91a54073cc75d9ae3b8855637dfe3cb195cbc628e3b811e4fe74c8b46282b93b42d89edeccec2593ab0070899a366cb6dd14d295a98d04e3113e670745a7f63e0535c4ea61df3feb26acfe0afdd7ebd59414fc42f3499990b6a232d536f88fbff656303d35daab4b6a30309f63a2ebae8ce633b9314176034cc824ec9b5e26d422bbb0f5696092fb6bfe4cb2c8681fcd96b701528f009ba19e9a9479656e674be5da5767d9b399eeba8db84b8e3d47a2352f00ead76e3f36b947804a6812051d475ea086c4c0bde7bc08da2a711ee7c64a8e8173898981b1f9159804ac52a81e786316bc70341d60109499d6234ad699a081a5738e70d686aa5a9bd3d4a8c79d8e163cd55bccbf872ffdbcbbbe61530f5d74b261a4f9adcf2bf75c129972e10dddc5e579cf55d76da4cd380283c4834e0411296f5046be815306a0984a17b33f46fca8e154b05f816c20387afc3fc8058b51865db734d8f6ab4a6e4ae0fdc01630ed9952f1bd2a9df904df26881970f14dbcc87e6f47e8a6229c0279f4d1570282134e6f8d741f9a50686f6fb3e6b20f8c566fb97689fd1361468ed4993c1f8ab28005b6e3ca86964d23288e51b3ba3beed2e7ce073f84f0086a2ae30fcf1f0f6588c21b3e41d5a4ae9e3cb084280ef7c82efa691b1602604356d69953453a33b61abedc621f61a37f3aff58ac845af7e1ae620f007975275d5a9bdb9c7c116fa5c9e4956d8ed1e68bb61f8325592b93556ba7142e27d3440f973ac31c34cd627f11983b9fe2d383565ecb2596c59c83032e6f6fb69c278534cc5d7ce7c1546e398e430c42aa522e994cd05cb9db674fe56d9da0d2551ed2e94f106071f6ea93db72b1000f4417b764bd0cac5fc3ca4536cd53caeaa49a8bc28c068385df109fdd346e02a53526f025b14d5353ca160a7d0b193c9fc63246db45d8959f28fdd4b73c3c590f35f93b6935ee66fb73860dea49b144fd3255197b4bc1d60c78f1c8fa3ddd4a4524c62d7a2678911c37c7e94ee2ceaa752010de696cd82f7be3602b395b2a11ab6109d0080e962a461ff82145b56d1fbbba430a877f2f8d38341c36fa240b725f592b7f248bb0fc1e6cd18154338a859da42035d86fbccaf67337758162315e9f6d9eb5554a93da23d20509ddbb873cce49a681bc31261947f4b7df5dbe4064f2917d702727e8be3331fad7131c3ce7f79f8146d8f084dc1a1462257e86ac7b6f046ea9b53be3307c6da26d18a33db328203884798768486f54ae098baf0de9ab5e325f3370d6927ebc223d2cfbe7496a24837de741c8469948d0ad460d4989cf6bda7732a4bab7accec4ad82e7b8ffb9ccdc54147637778e9007f3316c7dd99cad1af2c841607e06dd5128d53b0c5f1f5f6fa2ce224d215f5707fc39b50d1c3e28c9ccbd5f882f0b8a0b61e699012fa515dbea7eac6b6fad8bed7f4dcdf057485c2de87e1973fb8ad622702d0809e57094d53520ab66b7426d5eb640266dd67fcd195961a46d3a5ac252e20966692461e3a0837eb5a730afeeae10d453d423a4651c3c3d609e16477d7922367aceca2fab82ddeb771bc64daaa1beb65993f527881ef3f827b04e79bebf10cf35f9a28bae73fb18a969c5cb1c925ae52b66486a2a9031b820038f8352f7a273824c5b115463f624933828d6f310cc64116d71034a3094e4473d41102b35a146734dbad78867000d687464b4ec5baa55766b792324fe79dee6ea040ef515abcdd5a959ce73daed9be635ece268b813ef813c8bdc4d7df2303807d678f2a7e673f823a51e267da74c55070d980cf7068923c6311f62b6042c1ba2ace601fd029e069e7864b8629ff339bb373cab71b8c32f112ed70f8b006f7457a653174fe5279fce100fc6f17d6845ee4726f1a1cbfa648d03e703b5e3ab279d22b08f1e62542cb2fb0571c7c1e75d574e682bfcc90142a3b948f791a15809fc713b9babde9f23cad1e4aca8698e5b25dbc3686903e302a351cabd3e8f58ebfe28bcc706e28051d606dfe9771f522d3717862ecb8d65c9b99861c1f2401d7d3853c124b7291e6aa0ae065a2a33f4991acc5c7ee7528d53d22e73d6569c5bba3cb8282b45ad3999cee5f30c2c78296372e433e220e2a7ab193c54f25753b89bc415111b17b7cb5f2e61f3bd1a6fa8c70784d22b45d437a1b2df7b59eda471be3880f7537ce202dbc5cbae84e230d53fdb648325f05d1c74d93fa50bddea19124988837ada0a686f809cd28acb048551ee7c944
@@ -13,30 +13,22 @@ import { encrypt, decrypt, isEncrypted } from '../components/config/crypto';
13
13
  * - `decrypt` writes atomically to the plain filename (when given a `.enc` file it writes to the base name)
14
14
  */
15
15
  const [, , command, targetPath, argKey] = process.argv;
16
+ // Helper: obtain key from arg or env or .env.local (only used for encrypt/decrypt)
16
17
  let key = argKey || process.env.PIXELATED_CONFIG_KEY;
17
- // If key is still missing, try to load it from .env.local
18
18
  if (!key) {
19
19
  const envPath = path.join(process.cwd(), '.env.local');
20
20
  if (fs.existsSync(envPath)) {
21
- const envContent = fs.readFileSync(envPath, 'utf8');
22
- const match = envContent.match(/^PIXELATED_CONFIG_KEY=(.*)$/m);
23
- if (match && match[1]) {
24
- key = match[1].trim();
21
+ try {
22
+ const envContent = fs.readFileSync(envPath, 'utf8');
23
+ const match = envContent.match(/^PIXELATED_CONFIG_KEY=(.*)$/m);
24
+ if (match && match[1])
25
+ key = match[1].trim();
26
+ }
27
+ catch (e) {
28
+ // ignore
25
29
  }
26
30
  }
27
31
  }
28
- if (!command || !targetPath || !key) {
29
- console.log('Usage:');
30
- console.log(' encrypt <filePath> [key] - Encrypts the file and writes `<filePath>.enc`');
31
- console.log(' decrypt <filePath> [key] - Decrypts the file and writes the plaintext file (atomic write)');
32
- console.log('\nNote: Key can be passed as argument or via PIXELATED_CONFIG_KEY env var.');
33
- process.exit(1);
34
- }
35
- const fullPath = path.isAbsolute(targetPath) ? targetPath : path.resolve(process.cwd(), targetPath);
36
- if (!fs.existsSync(fullPath)) {
37
- console.error(`File not found: ${fullPath}`);
38
- process.exit(1);
39
- }
40
32
  const atomicWrite = (destPath, data) => {
41
33
  const dir = path.dirname(destPath);
42
34
  const base = path.basename(destPath);
@@ -44,29 +36,148 @@ const atomicWrite = (destPath, data) => {
44
36
  fs.writeFileSync(tmp, data, 'utf8');
45
37
  fs.renameSync(tmp, destPath);
46
38
  };
39
+ // Helper: print usage/help
40
+ function printUsage() {
41
+ console.log('Usage:');
42
+ console.log(' npx tsx src/scripts/config-vault.ts encrypt <filePath> [key] - Encrypts <filePath> (writes <filePath>.enc)');
43
+ console.log(' npx tsx src/scripts/config-vault.ts decrypt <filePath> [key] - Decrypts <filePath>.enc and writes plaintext');
44
+ console.log(' npx tsx src/scripts/config-vault.ts postbuild - CI helper: decrypts and injects into .next/server');
45
+ console.log('\nNotes:');
46
+ console.log(' - Key can be passed as argument or via PIXELATED_CONFIG_KEY env var.');
47
+ console.log(' - Use PIXELATED_CONFIG_DEBUG=1 for verbose output during postbuild.');
48
+ }
49
+ // Helpful messages when arguments are missing
50
+ if (!command) {
51
+ console.log('No command provided.');
52
+ printUsage();
53
+ process.exit(0);
54
+ }
55
+ if (command === 'help' || command === '--help' || command === '-h') {
56
+ printUsage();
57
+ process.exit(0);
58
+ }
59
+ // If encrypt/decrypt are requested, ensure targetPath and key exist; log informative messages
60
+ if (command === 'encrypt' || command === 'decrypt') {
61
+ if (!targetPath) {
62
+ console.log('No target path provided for encrypt/decrypt.');
63
+ printUsage();
64
+ process.exit(1);
65
+ }
66
+ if (!key) {
67
+ // we attempted to resolve key from env/.env.local earlier; if still missing, inform the user
68
+ console.log('No key provided for encrypt/decrypt (argument, PIXELATED_CONFIG_KEY, or .env.local).');
69
+ printUsage();
70
+ process.exit(1);
71
+ }
72
+ }
73
+ /**
74
+ * Post-build behavior used by CI (Amplify):
75
+ * - Look for an encrypted config file in standard candidate locations
76
+ * - Validate PIXELATED_CONFIG_KEY (from env or .env.local)
77
+ * - Decrypt in-place and copy plaintext to .next/server/pixelated.config.json
78
+ * - Validate JSON and emit a concise success message
79
+ */
80
+ function decryptPostBuild() {
81
+ const DEBUG = process.env.PIXELATED_CONFIG_DEBUG === '1';
82
+ const candidates = [
83
+ path.join(process.cwd(), 'src/app/config/pixelated.config.json.enc'),
84
+ path.join(process.cwd(), 'src/config/pixelated.config.json.enc'),
85
+ path.join(process.cwd(), 'src/pixelated.config.json.enc'),
86
+ ];
87
+ let foundEnc = null;
88
+ for (const p of candidates) {
89
+ if (fs.existsSync(p)) {
90
+ foundEnc = p;
91
+ break;
92
+ }
93
+ }
94
+ if (!foundEnc) {
95
+ if (DEBUG)
96
+ console.log('No encrypted config found; nothing to do.');
97
+ process.exit(0);
98
+ }
99
+ // Resolve key (env preferred, then .env.local)
100
+ let keyLocal = process.env.PIXELATED_CONFIG_KEY;
101
+ if (!keyLocal) {
102
+ const envPath = path.join(process.cwd(), '.env.local');
103
+ if (fs.existsSync(envPath)) {
104
+ try {
105
+ const envContent = fs.readFileSync(envPath, 'utf8');
106
+ const match = envContent.match(/^PIXELATED_CONFIG_KEY=(.*)$/m);
107
+ if (match && match[1])
108
+ keyLocal = match[1].trim();
109
+ }
110
+ catch (e) {
111
+ // ignore
112
+ }
113
+ }
114
+ }
115
+ if (!keyLocal) {
116
+ console.error('PIXELATED_CONFIG_KEY not set; cannot decrypt config.');
117
+ process.exit(1);
118
+ }
119
+ if (!/^[0-9a-fA-F]{64}$/.test(keyLocal)) {
120
+ console.error('PIXELATED_CONFIG_KEY invalid: must be 64 hex characters.');
121
+ process.exit(1);
122
+ }
123
+ try {
124
+ const raw = fs.readFileSync(foundEnc, 'utf8');
125
+ if (!isEncrypted(raw)) {
126
+ console.error('Found file is not in encrypted format.');
127
+ process.exit(1);
128
+ }
129
+ const decrypted = decrypt(raw, keyLocal);
130
+ const dest = foundEnc.endsWith('.enc') ? foundEnc.slice(0, -4) : `${foundEnc}.plain`;
131
+ atomicWrite(dest, decrypted);
132
+ // Copy to .next/server for SSR to pick up
133
+ const injectPath = path.join(process.cwd(), '.next', 'server', 'pixelated.config.json');
134
+ fs.mkdirSync(path.dirname(injectPath), { recursive: true });
135
+ fs.copyFileSync(dest, injectPath);
136
+ // Validate JSON
137
+ JSON.parse(decrypted);
138
+ console.log('Config injected into .next/server/pixelated.config.json');
139
+ if (DEBUG)
140
+ console.log(`Decrypted ${path.basename(foundEnc)} -> ${injectPath}`);
141
+ process.exit(0);
142
+ }
143
+ catch (err) {
144
+ console.error(`Post-build decrypt failed: ${err.message}`);
145
+ process.exit(1);
146
+ }
147
+ }
47
148
  try {
48
149
  if (command === 'encrypt') {
49
- const content = fs.readFileSync(fullPath, 'utf8');
150
+ if (!key) {
151
+ console.error('Encryption key is required.');
152
+ process.exit(1);
153
+ }
154
+ const content = fs.readFileSync(targetPath, 'utf8');
50
155
  if (isEncrypted(content)) {
51
156
  console.log('File is already encrypted. No action taken.');
52
157
  process.exit(0);
53
158
  }
54
159
  const encrypted = encrypt(content, key);
55
- const encPath = fullPath.endsWith('.enc') ? fullPath : `${fullPath}.enc`;
160
+ const encPath = targetPath.endsWith('.enc') ? targetPath : `${targetPath}.enc`;
56
161
  atomicWrite(encPath, encrypted);
57
162
  console.log(`Successfully encrypted ${targetPath} -> ${path.basename(encPath)}`);
58
163
  }
59
164
  else if (command === 'decrypt') {
60
- const content = fs.readFileSync(fullPath, 'utf8');
165
+ if (!key) {
166
+ console.error('Decryption key is required.');
167
+ process.exit(1);
168
+ }
169
+ const content = fs.readFileSync(targetPath, 'utf8');
61
170
  if (!isEncrypted(content)) {
62
171
  console.log('File is not encrypted. No action taken.');
63
172
  process.exit(0);
64
173
  }
65
174
  const decrypted = decrypt(content, key);
66
- // Destination: if input ends with .enc, strip it; otherwise overwrite the provided path
67
- const destPath = fullPath.endsWith('.enc') ? fullPath.slice(0, -4) : fullPath;
175
+ const destPath = targetPath.endsWith('.enc') ? targetPath.slice(0, -4) : targetPath;
68
176
  atomicWrite(destPath, decrypted);
69
- console.log(`Successfully decrypted ${path.basename(fullPath)} -> ${path.basename(destPath)}`);
177
+ console.log(`Successfully decrypted ${path.basename(targetPath)} -> ${path.basename(destPath)}`);
178
+ }
179
+ else if (command === 'postbuild' || command === 'post-build' || command === 'inject') {
180
+ decryptPostBuild();
70
181
  }
71
182
  else {
72
183
  console.error(`Unknown command: ${command}`);
@@ -15,35 +15,22 @@ import { encrypt, decrypt, isEncrypted } from '../components/config/crypto';
15
15
  */
16
16
 
17
17
  const [,, command, targetPath, argKey] = process.argv;
18
- let key = argKey || process.env.PIXELATED_CONFIG_KEY;
19
18
 
20
- // If key is still missing, try to load it from .env.local
19
+ // Helper: obtain key from arg or env or .env.local (only used for encrypt/decrypt)
20
+ let key = argKey || process.env.PIXELATED_CONFIG_KEY;
21
21
  if (!key) {
22
22
  const envPath = path.join(process.cwd(), '.env.local');
23
23
  if (fs.existsSync(envPath)) {
24
- const envContent = fs.readFileSync(envPath, 'utf8');
25
- const match = envContent.match(/^PIXELATED_CONFIG_KEY=(.*)$/m);
26
- if (match && match[1]) {
27
- key = match[1].trim();
24
+ try {
25
+ const envContent = fs.readFileSync(envPath, 'utf8');
26
+ const match = envContent.match(/^PIXELATED_CONFIG_KEY=(.*)$/m);
27
+ if (match && match[1]) key = match[1].trim();
28
+ } catch (e) {
29
+ // ignore
28
30
  }
29
31
  }
30
32
  }
31
33
 
32
- if (!command || !targetPath || !key) {
33
- console.log('Usage:');
34
- console.log(' encrypt <filePath> [key] - Encrypts the file and writes `<filePath>.enc`');
35
- console.log(' decrypt <filePath> [key] - Decrypts the file and writes the plaintext file (atomic write)');
36
- console.log('\nNote: Key can be passed as argument or via PIXELATED_CONFIG_KEY env var.');
37
- process.exit(1);
38
- }
39
-
40
- const fullPath = path.isAbsolute(targetPath) ? targetPath : path.resolve(process.cwd(), targetPath);
41
-
42
- if (!fs.existsSync(fullPath)) {
43
- console.error(`File not found: ${fullPath}`);
44
- process.exit(1);
45
- }
46
-
47
34
  const atomicWrite = (destPath: string, data: string) => {
48
35
  const dir = path.dirname(destPath);
49
36
  const base = path.basename(destPath);
@@ -52,28 +39,150 @@ const atomicWrite = (destPath: string, data: string) => {
52
39
  fs.renameSync(tmp, destPath);
53
40
  };
54
41
 
42
+ // Helper: print usage/help
43
+ function printUsage(): void {
44
+ console.log('Usage:');
45
+ console.log(' npx tsx src/scripts/config-vault.ts encrypt <filePath> [key] - Encrypts <filePath> (writes <filePath>.enc)');
46
+ console.log(' npx tsx src/scripts/config-vault.ts decrypt <filePath> [key] - Decrypts <filePath>.enc and writes plaintext');
47
+ console.log(' npx tsx src/scripts/config-vault.ts postbuild - CI helper: decrypts and injects into .next/server');
48
+ console.log('\nNotes:');
49
+ console.log(' - Key can be passed as argument or via PIXELATED_CONFIG_KEY env var.');
50
+ console.log(' - Use PIXELATED_CONFIG_DEBUG=1 for verbose output during postbuild.');
51
+ }
52
+
53
+ // Helpful messages when arguments are missing
54
+ if (!command) {
55
+ console.log('No command provided.');
56
+ printUsage();
57
+ process.exit(0);
58
+ }
59
+ if (command === 'help' || command === '--help' || command === '-h') {
60
+ printUsage();
61
+ process.exit(0);
62
+ }
63
+
64
+ // If encrypt/decrypt are requested, ensure targetPath and key exist; log informative messages
65
+ if (command === 'encrypt' || command === 'decrypt') {
66
+ if (!targetPath) {
67
+ console.log('No target path provided for encrypt/decrypt.');
68
+ printUsage();
69
+ process.exit(1);
70
+ }
71
+ if (!key) {
72
+ // we attempted to resolve key from env/.env.local earlier; if still missing, inform the user
73
+ console.log('No key provided for encrypt/decrypt (argument, PIXELATED_CONFIG_KEY, or .env.local).');
74
+ printUsage();
75
+ process.exit(1);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Post-build behavior used by CI (Amplify):
81
+ * - Look for an encrypted config file in standard candidate locations
82
+ * - Validate PIXELATED_CONFIG_KEY (from env or .env.local)
83
+ * - Decrypt in-place and copy plaintext to .next/server/pixelated.config.json
84
+ * - Validate JSON and emit a concise success message
85
+ */
86
+ function decryptPostBuild(): void {
87
+ const DEBUG = process.env.PIXELATED_CONFIG_DEBUG === '1';
88
+ const candidates = [
89
+ path.join(process.cwd(), 'src/app/config/pixelated.config.json.enc'),
90
+ path.join(process.cwd(), 'src/config/pixelated.config.json.enc'),
91
+ path.join(process.cwd(), 'src/pixelated.config.json.enc'),
92
+ ];
93
+
94
+ let foundEnc: string | null = null;
95
+ for (const p of candidates) {
96
+ if (fs.existsSync(p)) {
97
+ foundEnc = p;
98
+ break;
99
+ }
100
+ }
101
+
102
+ if (!foundEnc) {
103
+ if (DEBUG) console.log('No encrypted config found; nothing to do.');
104
+ process.exit(0);
105
+ }
106
+
107
+ // Resolve key (env preferred, then .env.local)
108
+ let keyLocal = process.env.PIXELATED_CONFIG_KEY;
109
+ if (!keyLocal) {
110
+ const envPath = path.join(process.cwd(), '.env.local');
111
+ if (fs.existsSync(envPath)) {
112
+ try {
113
+ const envContent = fs.readFileSync(envPath, 'utf8');
114
+ const match = envContent.match(/^PIXELATED_CONFIG_KEY=(.*)$/m);
115
+ if (match && match[1]) keyLocal = match[1].trim();
116
+ } catch (e) {
117
+ // ignore
118
+ }
119
+ }
120
+ }
121
+
122
+ if (!keyLocal) {
123
+ console.error('PIXELATED_CONFIG_KEY not set; cannot decrypt config.');
124
+ process.exit(1);
125
+ }
126
+ if (!/^[0-9a-fA-F]{64}$/.test(keyLocal)) {
127
+ console.error('PIXELATED_CONFIG_KEY invalid: must be 64 hex characters.');
128
+ process.exit(1);
129
+ }
130
+
131
+ try {
132
+ const raw = fs.readFileSync(foundEnc, 'utf8');
133
+ if (!isEncrypted(raw)) {
134
+ console.error('Found file is not in encrypted format.');
135
+ process.exit(1);
136
+ }
137
+ const decrypted = decrypt(raw, keyLocal);
138
+ const dest = foundEnc.endsWith('.enc') ? foundEnc.slice(0, -4) : `${foundEnc}.plain`;
139
+ atomicWrite(dest, decrypted);
140
+ // Copy to .next/server for SSR to pick up
141
+ const injectPath = path.join(process.cwd(), '.next', 'server', 'pixelated.config.json');
142
+ fs.mkdirSync(path.dirname(injectPath), { recursive: true });
143
+ fs.copyFileSync(dest, injectPath);
144
+ // Validate JSON
145
+ JSON.parse(decrypted);
146
+ console.log('Config injected into .next/server/pixelated.config.json');
147
+ if (DEBUG) console.log(`Decrypted ${path.basename(foundEnc)} -> ${injectPath}`);
148
+ process.exit(0);
149
+ } catch (err: any) {
150
+ console.error(`Post-build decrypt failed: ${err.message}`);
151
+ process.exit(1);
152
+ }
153
+ }
154
+
55
155
  try {
56
156
  if (command === 'encrypt') {
57
- const content = fs.readFileSync(fullPath, 'utf8');
157
+ if (!key) {
158
+ console.error('Encryption key is required.');
159
+ process.exit(1);
160
+ }
161
+ const content = fs.readFileSync(targetPath, 'utf8');
58
162
  if (isEncrypted(content)) {
59
163
  console.log('File is already encrypted. No action taken.');
60
164
  process.exit(0);
61
165
  }
62
166
  const encrypted = encrypt(content, key);
63
- const encPath = fullPath.endsWith('.enc') ? fullPath : `${fullPath}.enc`;
167
+ const encPath = targetPath.endsWith('.enc') ? targetPath : `${targetPath}.enc`;
64
168
  atomicWrite(encPath, encrypted);
65
169
  console.log(`Successfully encrypted ${targetPath} -> ${path.basename(encPath)}`);
66
170
  } else if (command === 'decrypt') {
67
- const content = fs.readFileSync(fullPath, 'utf8');
171
+ if (!key) {
172
+ console.error('Decryption key is required.');
173
+ process.exit(1);
174
+ }
175
+ const content = fs.readFileSync(targetPath, 'utf8');
68
176
  if (!isEncrypted(content)) {
69
177
  console.log('File is not encrypted. No action taken.');
70
178
  process.exit(0);
71
179
  }
72
180
  const decrypted = decrypt(content, key);
73
- // Destination: if input ends with .enc, strip it; otherwise overwrite the provided path
74
- const destPath = fullPath.endsWith('.enc') ? fullPath.slice(0, -4) : fullPath;
181
+ const destPath = targetPath.endsWith('.enc') ? targetPath.slice(0, -4) : targetPath;
75
182
  atomicWrite(destPath, decrypted);
76
- console.log(`Successfully decrypted ${path.basename(fullPath)} -> ${path.basename(destPath)}`);
183
+ console.log(`Successfully decrypted ${path.basename(targetPath)} -> ${path.basename(destPath)}`);
184
+ } else if (command === 'postbuild' || command === 'post-build' || command === 'inject') {
185
+ decryptPostBuild();
77
186
  } else {
78
187
  console.error(`Unknown command: ${command}`);
79
188
  process.exit(1);
@@ -79,6 +79,8 @@ const __filename = fileURLToPath(import.meta.url);
79
79
  const __dirname = path.dirname(__filename);
80
80
 
81
81
  const exec = promisify(execCb);
82
+ // Exportable exec wrapper so tests can stub it.
83
+ export let _exec = exec;
82
84
 
83
85
  async function exists(p) {
84
86
  try {
@@ -232,6 +234,112 @@ export async function copyTemplateForPage(templatePathArg, templateSrc, template
232
234
  }
233
235
 
234
236
 
237
+ export async function createAndPushRemote(destPath, siteName, defaultOwner) {
238
+ // Initialize a local git repo and make the initial commit
239
+ await _exec('git init -b main', { cwd: destPath });
240
+ await _exec('git add .', { cwd: destPath });
241
+ await _exec('git commit -m "chore: initial commit from pixelated-template"', { cwd: destPath });
242
+ console.log('✅ Git initialized and initial commit created.');
243
+
244
+ // If an encrypted config exists, attempt a non-fatal decrypt in the new site to ensure the token can be read
245
+ const encCandidates = [
246
+ path.join(destPath, 'src', 'app', 'config', 'pixelated.config.json.enc'),
247
+ path.join(destPath, 'src', 'config', 'pixelated.config.json.enc'),
248
+ path.join(destPath, 'src', 'pixelated.config.json.enc'),
249
+ path.join(destPath, 'pixelated.config.json.enc'),
250
+ path.join(destPath, 'dist', 'config', 'pixelated.config.json.enc')
251
+ ];
252
+ for (const p of encCandidates) {
253
+ if (await exists(p)) {
254
+ console.log(`Found encrypted config at ${p}. Attempting to run 'npm run config:decrypt' in the new site (non-fatal)`);
255
+ try {
256
+ await _exec('npm run config:decrypt', { cwd: destPath, timeout: 60_000 });
257
+ console.log('Attempted config:decrypt (non-fatal)');
258
+ } catch (err) {
259
+ console.warn('config:decrypt failed or PIXELATED_CONFIG_KEY missing (non-fatal):', err?.message || err);
260
+ }
261
+ break;
262
+ }
263
+ }
264
+
265
+ // Create a small temporary script inside the new site to reliably import the project's provider and print JSON to stdout
266
+ const tmpDir = path.join(destPath, '.px-scripts');
267
+ const tmpFile = path.join(tmpDir, 'get_github_token.ts');
268
+ await fs.mkdir(tmpDir, { recursive: true });
269
+ const tmpContent = `import('./src/components/config/config').then(m => {
270
+ const cfg = m.getFullPixelatedConfig();
271
+ // Only print the github object (or null) as JSON to stdout
272
+ console.log(JSON.stringify(cfg?.github || null));
273
+ }).catch(e => {
274
+ console.error('ERR_IMPORT', e?.message || e);
275
+ process.exit(2);
276
+ });`;
277
+ await fs.writeFile(tmpFile, tmpContent, 'utf8');
278
+
279
+ let execOut = null;
280
+ try {
281
+ execOut = await _exec(`npx tsx ${tmpFile}`, { cwd: destPath, timeout: 60_000 });
282
+ } catch (e) {
283
+ // Provide a helpful error message and ensure cleanup happens below
284
+ console.error('❌ Failed to run config provider to obtain GitHub token. Ensure PIXELATED_CONFIG_KEY is available (e.g., in .env.local) and the site includes an encrypted pixelated.config.json.enc');
285
+ throw e;
286
+ } finally {
287
+ // Always clean up the temporary script directory
288
+ try { await fs.rm(tmpDir, { recursive: true, force: true }); } catch (_) { /* ignore cleanup errors */ }
289
+ }
290
+
291
+ const outStr = (execOut && execOut.stdout) ? String(execOut.stdout).trim() : '';
292
+ if (!outStr) {
293
+ console.error('❌ No output from config provider; cannot locate github token');
294
+ throw new Error('Missing provider output');
295
+ }
296
+
297
+ let githubInfo = null;
298
+ try { githubInfo = JSON.parse(outStr); } catch (e) { console.error('❌ Invalid JSON from config provider:', outStr); throw e; }
299
+ const token = githubInfo?.token;
300
+ const cfgOwner = githubInfo?.defaultOwner;
301
+ if (!token) {
302
+ console.error('❌ github.token not found in decrypted config; cannot create remote repo.');
303
+ throw new Error('Missing github.token');
304
+ }
305
+
306
+ const repoName = siteName;
307
+ const ownerForMessage = cfgOwner || defaultOwner;
308
+ console.log(`Creating GitHub repo: ${ownerForMessage}/${repoName} ...`);
309
+
310
+ let resp;
311
+ try {
312
+ resp = await fetch('https://api.github.com/user/repos', {
313
+ method: 'POST',
314
+ headers: {
315
+ 'Authorization': `token ${token}`,
316
+ 'Content-Type': 'application/json',
317
+ 'User-Agent': 'create-pixelated-app'
318
+ },
319
+ body: JSON.stringify({ name: repoName, private: false })
320
+ });
321
+ } catch (e) {
322
+ console.error('❌ Failed to call GitHub API', e?.message || e);
323
+ throw e;
324
+ }
325
+
326
+ const body = await (async () => { try { return await resp.json(); } catch (e) { return null; } })();
327
+ if (!resp.ok) {
328
+ console.error(`❌ Failed to create GitHub repo: ${resp.status} ${resp.statusText} ${body?.message || ''}`);
329
+ throw new Error('GitHub repo creation failed');
330
+ }
331
+ const cloneUrl = body.clone_url;
332
+ if (!cloneUrl) {
333
+ console.error('❌ GitHub returned unexpected response (no clone_url)');
334
+ throw new Error('Invalid GitHub response');
335
+ }
336
+
337
+ // Add remote and push
338
+ await _exec(`git remote add origin ${cloneUrl}`, { cwd: destPath });
339
+ await _exec('git branch --show-current || git branch -M main', { cwd: destPath });
340
+ await _exec('git push -u origin main', { cwd: destPath });
341
+ console.log(`✅ Remote created and initial commit pushed: ${cloneUrl}`);
342
+ }
235
343
  async function main() {
236
344
  const rl = readline.createInterface({ input, output });
237
345
  try {
@@ -435,16 +543,25 @@ async function main() {
435
543
  console.warn('⚠️ Failed to replace placeholders in site copy:', e?.message || e);
436
544
  }
437
545
  }
438
- // Prompt about git initialization
439
- const initGitAnswer = (await rl.question('Initialize a fresh git repository here? (Y/n): ')) || 'y';
440
- if (initGitAnswer.toLowerCase() === 'y' || initGitAnswer.toLowerCase() === 'yes') {
546
+ // Prompt about creating a new GitHub repository. Default owner is read from components config `github.defaultOwner` (fallback: 'brianwhaley')
547
+ const componentsCfgPath = path.resolve(__dirname, '..', 'config', 'pixelated.config.json');
548
+ let defaultOwner = 'brianwhaley';
549
+ try {
550
+ if (await exists(componentsCfgPath)) {
551
+ const compCfgText = await fs.readFile(componentsCfgPath, 'utf8');
552
+ const compCfg = JSON.parse(compCfgText);
553
+ if (compCfg?.github?.defaultOwner) defaultOwner = compCfg.github.defaultOwner;
554
+ }
555
+ } catch (e) {
556
+ // ignore and use fallback
557
+ }
558
+ const createRemoteAnswer = (await rl.question(`Create a new GitHub repository in '${defaultOwner}' and push the initial commit? (Y/n): `)) || 'y';
559
+ if (createRemoteAnswer.toLowerCase() === 'y' || createRemoteAnswer.toLowerCase() === 'yes') {
441
560
  try {
442
- await exec('git init -b main', { cwd: destPath });
443
- await exec('git add .', { cwd: destPath });
444
- await exec('git commit -m "chore: initial commit from pixelated-template"', { cwd: destPath });
445
- console.log('✅ Git initialized and initial commit created.');
561
+ await createAndPushRemote(destPath, siteName, defaultOwner);
446
562
  } catch (e) {
447
- console.warn('⚠️ Git init or commit failed. You can initialize manually later.', e?.stderr || e?.message || e);
563
+ console.warn('⚠️ Repo creation or git push failed. Your local repository is still available at:', destPath);
564
+ console.warn(e?.stderr || e?.message || e);
448
565
  }
449
566
  }
450
567
 
@@ -469,3 +586,4 @@ if (typeof process !== 'undefined' && new URL(import.meta.url).pathname === proc
469
586
  // CLI entry point: run the interactive main flow
470
587
  main();
471
588
  }
589
+
@@ -0,0 +1,590 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * create-pixelated-app.js
4
+ *
5
+ * Simple CLI to scaffold a new site from `pixelated-template`.
6
+ * - copies the template to a destination folder
7
+ * - clears out the .git history in the copy
8
+ * - optionally initializes a fresh git repo and adds a remote
9
+ *
10
+ * TODOs (placeholders for later work):
11
+ * - Create/patch pixelated.config.json and optionally run config:encrypt
12
+ * - Run `npm ci` / `npm run lint` / `npm test` and optionally build
13
+ * - Optionally create GitHub repo using API (with token from secure vault)
14
+ *
15
+ *
16
+ 1) Recommended approach (short) ✅
17
+ Build a small Node-based CLI (easier cross-platform than a Bash script) called e.g. scripts/new-site.js or an npm create script.
18
+ Make it interactive with sensible CLI flags (--name, --domain, --repo, --git, --encrypt, --no-install) and a non-interactive mode for CI.
19
+
20
+ 2) High-level workflow the script should perform 🔁
21
+ Validate inputs (target path, site slug, repo name).
22
+ Copy pixelated-template → ./<new-site-name> (preserve file modes).
23
+ Replace placeholders in files (template tokens like {{SITE_NAME}}, {{PACKAGE_NAME}}, {{DOMAIN}}).
24
+ Patch template-specific files (see list below).
25
+ Remove template git metadata (rm -rf .git) and reset any CI state.
26
+ Create/patch pixelated.config.json with site-specific values (ask for secrets). Optionally run npm run config:encrypt to write .enc.
27
+ Run validation: npm ci (optional), npm run lint, npm test, npm run build.
28
+ Init git, make initial commit, optionally create remote GitHub repo (if token available) and push.
29
+ Print summary and next steps (e.g., configure hosting / DNS / deploy keys).
30
+
31
+ 3) Files / fields to update in the template 🔧
32
+ package.json — name, description, repository, author, homepage, version, scripts (if you want to change default scripts).
33
+ README.md — project title and quick-start instructions.
34
+ next.config.ts / amplify.yml — site-specific env entries and build steps.
35
+ pixelated.config.json — update global/aws/contentful/google fields for this site (prefer *.enc workflow).
36
+ src/app/(pages)/… and any site metadata files (site slug, default pagesDir).
37
+ public/ assets and site-images.json — update site logos/URLs.
38
+ certificates/ if using TLS — template may include placeholder paths.
39
+ FEATURE_CHECKLIST.md — optionally update default checklist for new site.
40
+ .gitignore — ensure pixelated.config.json is ignored if plaintext during setup.
41
+
42
+ 4) Template hardening (recommended changes in pixelated-template) ⚠️
43
+ Add placeholder tokens for the things above (e.g., {{PACKAGE_NAME}}, {{SITE_DOMAIN}}) rather than literal values.
44
+ Add a template.json or template.meta that lists files & placeholders to auto-replace.
45
+ Ensure the template does not include real secrets. Provide pixelated.config.json.example with placeholders and an example .env.local.example.
46
+ Add a scripts/prepare-template.sh or test job that ensures no plaintext sensitive values remain.
47
+ Document the creation process in TEMPLATE_README.md for maintainers.
48
+
49
+ 5) Security & operational notes 🔐
50
+ Do not commit plaintext pixelated.config.json. If the CLI accepts secret values, optionally run npm run config:encrypt immediately and only commit the .enc if needed (prefer not to commit it; store in site secrets store).
51
+ Add an option to write the encrypted file directly into dist/config when building a deployment bundle.
52
+ Provide an audit step: scan resulting repo for secret patterns before final commit/push.
53
+
54
+ 6) Helpful CLI implementation details (tools & libs) 🛠️
55
+ Use Node + these packages: fs-extra (copy), replace-in-file or simple .replace() for template tokens, inquirer (prompt), execa (run commands), simple-git (git ops), node-fetch or @octokit/rest (optional GitHub repo creation).
56
+ Use safe file writes and atomic renames for config operations.
57
+ Provide a --dry-run and --preview mode so users can verify changes before creating repo or pushing.
58
+
59
+ 7) Validation & post-creation steps ✅
60
+ npm run lint && npm test && npm run build — fail fast and show errors for the new site.
61
+ Optional: run npm run config:decrypt locally (with provided key) to confirm decryption works in your deploy workflow (BUT DO NOT store the key in the repo).
62
+
63
+ 8) Example minimal CLI flow (pseudo)
64
+ Prompt: site name, package name, repo URL (optional), domain, author, contentful tokens, aws keys (optional), encrypt?
65
+ Copy template → replace tokens → create pixelated.config.json from pixelated.config.json.example → encrypt if requested → init git → run install/build → push to remote.
66
+
67
+ */
68
+
69
+ import fs from 'fs/promises';
70
+ import path from 'path';
71
+ import { exec as execCb } from 'child_process';
72
+ import { promisify } from 'util';
73
+ import readline from 'readline/promises';
74
+ import { stdin as input, stdout as output } from 'process';
75
+ import { fileURLToPath } from 'url';
76
+ import { loadManifest, findTemplateForSlug, pruneTemplateDirs, printAvailableTemplates } from './create-pixelated-app-template-mapper.js';
77
+
78
+ const __filename = fileURLToPath(import.meta.url);
79
+ const __dirname = path.dirname(__filename);
80
+
81
+ const exec = promisify(execCb);
82
+ // Exportable exec wrapper so tests can stub it.
83
+ export let _exec = exec;
84
+
85
+ async function exists(p) {
86
+ try {
87
+ await fs.access(p);
88
+ return true;
89
+ } catch (e) {
90
+ return false;
91
+ }
92
+ }
93
+
94
+ async function countFiles(src) {
95
+ let total = 0;
96
+ async function walk(p) {
97
+ const stat = await fs.lstat(p);
98
+ if (stat.isDirectory()) {
99
+ const items = await fs.readdir(p, { withFileTypes: true });
100
+ for (const item of items) {
101
+ await walk(path.join(p, item.name));
102
+ }
103
+ } else {
104
+ total++;
105
+ }
106
+ }
107
+ await walk(src);
108
+ return total;
109
+ }
110
+
111
+ function startSpinner(messageFn) {
112
+ if (!process.stdout.isTTY) return { stop: () => {} };
113
+ const frames = ['-', '\\', '|', '/'];
114
+ let i = 0;
115
+ const interval = setInterval(() => {
116
+ const msg = messageFn ? messageFn() : '';
117
+ process.stdout.write(`\r${frames[i % frames.length]} ${msg}`);
118
+ i++;
119
+ }, 100);
120
+ return {
121
+ stop: () => {
122
+ clearInterval(interval);
123
+ process.stdout.write('\r');
124
+ process.stdout.write('\n');
125
+ }
126
+ };
127
+ }
128
+
129
+ async function copyRecursive(src, dest, onFileCopied) {
130
+ // Recursive copy that reports per-file progress via onFileCopied.
131
+ await fs.mkdir(dest, { recursive: true });
132
+ const items = await fs.readdir(src, { withFileTypes: true });
133
+ for (const item of items) {
134
+ const s = path.join(src, item.name);
135
+ const d = path.join(dest, item.name);
136
+ const stat = await fs.lstat(s);
137
+ if (stat.isDirectory()) {
138
+ await copyRecursive(s, d, onFileCopied);
139
+ } else if (stat.isSymbolicLink()) {
140
+ try {
141
+ const link = await fs.readlink(s);
142
+ await fs.symlink(link, d);
143
+ if (onFileCopied) onFileCopied(d);
144
+ } catch (e) {
145
+ // ignore symlink failures
146
+ if (onFileCopied) onFileCopied(d);
147
+ }
148
+ } else {
149
+ // Regular file
150
+ await fs.copyFile(s, d);
151
+ if (onFileCopied) onFileCopied(d);
152
+ }
153
+ }
154
+ }
155
+
156
+ // Replace placeholders like {{SITE_NAME}}, {{SITE_URL}}, {{EMAIL_ADDRESS}} across the created site
157
+ // This supports both literal tags and simple regex patterns (for cases where the bundler
158
+ // transformed template tokens into JS expressions like {SITE_NAME}). Each replacement
159
+ // entry may have { tag, value, isRegex } where isRegex indicates `tag` is a regex string.
160
+ async function replacePlaceholders(rootDir, replacements) {
161
+ const ignoreDirs = new Set(['.git', 'node_modules', 'dist', 'coverage']);
162
+ let filesChanged = 0;
163
+ async function walk(p) {
164
+ const items = await fs.readdir(p, { withFileTypes: true });
165
+ for (const item of items) {
166
+ const entry = path.join(p, item.name);
167
+ if (item.isDirectory()) {
168
+ // Allow running against .next when explicitly targeting it, but skip when walking a template
169
+ if (item.name === '.next' && rootDir !== '.next') continue;
170
+ if (ignoreDirs.has(item.name)) continue;
171
+ await walk(entry);
172
+ } else {
173
+ try {
174
+ let content = await fs.readFile(entry, 'utf8');
175
+ let newContent = content;
176
+ for (const { tag, value, isRegex } of replacements) {
177
+ let re;
178
+ if (isRegex) {
179
+ re = new RegExp(tag, 'g');
180
+ } else {
181
+ re = new RegExp(tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
182
+ }
183
+ newContent = newContent.replace(re, value);
184
+ }
185
+ if (newContent !== content) {
186
+ await fs.writeFile(entry, newContent, 'utf8');
187
+ filesChanged++;
188
+ }
189
+ } catch (e) {
190
+ // Could be binary or unreadable - skip
191
+ }
192
+ }
193
+ }
194
+ }
195
+ await walk(rootDir);
196
+ return filesChanged;
197
+ }
198
+
199
+ // Token map used by the CLI: literal marker (e.g., "__SITE_NAME__") -> replacement value (populate during interactive prompts)
200
+ export const TOKEN_MAP = {
201
+ "__SITE_NAME__": '',
202
+ "__SITE_URL__": '',
203
+ "__EMAIL_ADDRESS__": ''
204
+ };
205
+
206
+ // Helper: add a route entry to the routes.json structure for a newly created page
207
+ export function addRouteEntry(routesJson, pageSlug, displayName, rootDisplayName) {
208
+ if (!routesJson || !Array.isArray(routesJson.routes)) return false;
209
+ const candidatePath = `/${pageSlug}`;
210
+ if (routesJson.routes.some(r => r.path === candidatePath)) return false;
211
+ const name = displayName.split(/\s+/).map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' ');
212
+ routesJson.routes.push({
213
+ "name": name,
214
+ "path": candidatePath,
215
+ "title": `${rootDisplayName} - ${displayName}`,
216
+ "description": "",
217
+ "keywords": ""
218
+ });
219
+ return true;
220
+ }
221
+
222
+ // Copy a template page into the target directory, resolving to the expected location inside the workspace template.
223
+ // Returns an object { used: 'template'|'fallback', src: <sourcePath> }
224
+ export async function copyTemplateForPage(templatePathArg, templateSrc, templatePagesHome, targetDir) {
225
+ const folderName = path.basename(templateSrc);
226
+ const srcTemplatePath = path.join(templatePathArg, 'src', 'app', '(pages)', folderName);
227
+ if (await exists(srcTemplatePath)) {
228
+ await copyRecursive(srcTemplatePath, targetDir);
229
+ return { used: 'template', src: srcTemplatePath };
230
+ } else {
231
+ await copyRecursive(templatePagesHome, targetDir);
232
+ return { used: 'fallback', src: templatePagesHome };
233
+ }
234
+ }
235
+
236
+
237
+ export async function createAndPushRemote(destPath, siteName, defaultOwner) {
238
+ // Initialize a local git repo and make the initial commit
239
+ await _exec('git init -b main', { cwd: destPath });
240
+ await _exec('git add .', { cwd: destPath });
241
+ await _exec('git commit -m "chore: initial commit from pixelated-template"', { cwd: destPath });
242
+ console.log('✅ Git initialized and initial commit created.');
243
+
244
+ // If an encrypted config exists, attempt a non-fatal decrypt in the new site to ensure the token can be read
245
+ const encCandidates = [
246
+ path.join(destPath, 'src', 'app', 'config', 'pixelated.config.json.enc'),
247
+ path.join(destPath, 'src', 'config', 'pixelated.config.json.enc'),
248
+ path.join(destPath, 'src', 'pixelated.config.json.enc'),
249
+ path.join(destPath, 'pixelated.config.json.enc'),
250
+ path.join(destPath, 'dist', 'config', 'pixelated.config.json.enc')
251
+ ];
252
+ for (const p of encCandidates) {
253
+ if (await exists(p)) {
254
+ console.log(`Found encrypted config at ${p}. Attempting to run 'npm run config:decrypt' in the new site (non-fatal)`);
255
+ try {
256
+ await _exec('npm run config:decrypt', { cwd: destPath, timeout: 60_000 });
257
+ console.log('Attempted config:decrypt (non-fatal)');
258
+ } catch (err) {
259
+ console.warn('config:decrypt failed or PIXELATED_CONFIG_KEY missing (non-fatal):', err?.message || err);
260
+ }
261
+ break;
262
+ }
263
+ }
264
+
265
+ // Create a small temporary script inside the new site to reliably import the project's provider and print JSON to stdout
266
+ const tmpDir = path.join(destPath, '.px-scripts');
267
+ const tmpFile = path.join(tmpDir, 'get_github_token.ts');
268
+ await fs.mkdir(tmpDir, { recursive: true });
269
+ const tmpContent = `import('./src/components/config/config').then(m => {
270
+ const cfg = m.getFullPixelatedConfig();
271
+ // Only print the github object (or null) as JSON to stdout
272
+ console.log(JSON.stringify(cfg?.github || null));
273
+ }).catch(e => {
274
+ console.error('ERR_IMPORT', e?.message || e);
275
+ process.exit(2);
276
+ });`;
277
+ await fs.writeFile(tmpFile, tmpContent, 'utf8');
278
+
279
+ let execOut = null;
280
+ try {
281
+ execOut = await _exec(`npx tsx ${tmpFile}`, { cwd: destPath, timeout: 60_000 });
282
+ } catch (e) {
283
+ // Provide a helpful error message and ensure cleanup happens below
284
+ console.error('❌ Failed to run config provider to obtain GitHub token. Ensure PIXELATED_CONFIG_KEY is available (e.g., in .env.local) and the site includes an encrypted pixelated.config.json.enc');
285
+ throw e;
286
+ } finally {
287
+ // Always clean up the temporary script directory
288
+ try { await fs.rm(tmpDir, { recursive: true, force: true }); } catch (_) { /* ignore cleanup errors */ }
289
+ }
290
+
291
+ const outStr = (execOut && execOut.stdout) ? String(execOut.stdout).trim() : '';
292
+ if (!outStr) {
293
+ console.error('❌ No output from config provider; cannot locate github token');
294
+ throw new Error('Missing provider output');
295
+ }
296
+
297
+ let githubInfo = null;
298
+ try { githubInfo = JSON.parse(outStr); } catch (e) { console.error('❌ Invalid JSON from config provider:', outStr); throw e; }
299
+ const token = githubInfo?.token;
300
+ const cfgOwner = githubInfo?.defaultOwner;
301
+ if (!token) {
302
+ console.error('❌ github.token not found in decrypted config; cannot create remote repo.');
303
+ throw new Error('Missing github.token');
304
+ }
305
+
306
+ const repoName = siteName;
307
+ const ownerForMessage = cfgOwner || defaultOwner;
308
+ console.log(`Creating GitHub repo: ${ownerForMessage}/${repoName} ...`);
309
+
310
+ let resp;
311
+ try {
312
+ resp = await fetch('https://api.github.com/user/repos', {
313
+ method: 'POST',
314
+ headers: {
315
+ 'Authorization': `token ${token}`,
316
+ 'Content-Type': 'application/json',
317
+ 'User-Agent': 'create-pixelated-app'
318
+ },
319
+ body: JSON.stringify({ name: repoName, private: false })
320
+ });
321
+ } catch (e) {
322
+ console.error('❌ Failed to call GitHub API', e?.message || e);
323
+ throw e;
324
+ }
325
+
326
+ const body = await (async () => { try { return await resp.json(); } catch (e) { return null; } })();
327
+ if (!resp.ok) {
328
+ console.error(`❌ Failed to create GitHub repo: ${resp.status} ${resp.statusText} ${body?.message || ''}`);
329
+ throw new Error('GitHub repo creation failed');
330
+ }
331
+ const cloneUrl = body.clone_url;
332
+ if (!cloneUrl) {
333
+ console.error('❌ GitHub returned unexpected response (no clone_url)');
334
+ throw new Error('Invalid GitHub response');
335
+ }
336
+
337
+ // Add remote and push
338
+ await _exec(`git remote add origin ${cloneUrl}`, { cwd: destPath });
339
+ await _exec('git branch --show-current || git branch -M main', { cwd: destPath });
340
+ await _exec('git push -u origin main', { cwd: destPath });
341
+ console.log(`✅ Remote created and initial commit pushed: ${cloneUrl}`);
342
+ }
343
+ async function main() {
344
+ const rl = readline.createInterface({ input, output });
345
+ try {
346
+ console.log('\n📦 Pixelated site creator — scaffold a new site from pixelated-template\n');
347
+
348
+ const defaultName = 'my-site';
349
+ const siteName = (await rl.question(`Root directory name (kebab-case) [${defaultName}]: `)) || defaultName;
350
+
351
+ // Display name used in route titles: convert kebab to Title Case
352
+ const rootDisplayName = siteName.split(/[-_]+/).map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' ');
353
+
354
+ // Additional site metadata for placeholder substitution
355
+ const siteUrl = (await rl.question('Site URL (e.g. https://example.com) [leave blank to skip]: ')).trim();
356
+ const emailAddress = (await rl.question('Contact email address [leave blank to skip]: ')).trim();
357
+
358
+ const workspaceRoot = path.resolve(__dirname, '..', '..', '..');
359
+ const templatePath = path.resolve(workspaceRoot, 'pixelated-template');
360
+ if (!(await exists(templatePath))) {
361
+ console.error(`\n❌ Template not found at ${templatePath}. Please ensure this tool is run inside the workspace that contains pixelated-template.`);
362
+ process.exit(1);
363
+ }
364
+
365
+ // Load manifest (if present)
366
+ const manifest = await loadManifest(__dirname);
367
+ // Note: available templates will be printed later just before prompting for pages
368
+
369
+
370
+ // Destination is implicitly the top-level Git folder + site name to avoid prompting for it
371
+ const destPath = path.resolve(workspaceRoot, siteName);
372
+ console.log(`\nThe new site will be created at: ${destPath}`);
373
+ const proceed = (await rl.question('Proceed? (Y/n): ')) || 'y';
374
+ if (proceed.toLowerCase() !== 'y' && proceed.toLowerCase() !== 'yes') {
375
+ console.log('Aborting.');
376
+ process.exit(0);
377
+ }
378
+
379
+ if (await exists(destPath)) {
380
+ const shouldOverwrite = (await rl.question(`Destination ${destPath} already exists. Overwrite? (y/N): `)).toLowerCase();
381
+ if (shouldOverwrite !== 'y' && shouldOverwrite !== 'yes') {
382
+ console.log('Aborting. Choose another destination.');
383
+ process.exit(0);
384
+ }
385
+ console.log(`Removing existing directory ${destPath}...`);
386
+ await fs.rm(destPath, { recursive: true, force: true });
387
+ }
388
+
389
+ console.log(`\nCopying template from ${templatePath} -> ${destPath} ...`);
390
+ const totalFiles = await countFiles(templatePath);
391
+ let filesCopied = 0;
392
+ let lastFile = '';
393
+ const spinner = startSpinner(() => `Copying... ${filesCopied}/${totalFiles} ${lastFile ? '- ' + path.basename(lastFile) : ''}`);
394
+ await copyRecursive(templatePath, destPath, (f) => { if (f) { filesCopied++; lastFile = f; } });
395
+ // If fs.cp was used, per-file callbacks won't have been called; ensure we report the full total
396
+ if (filesCopied < totalFiles) filesCopied = totalFiles;
397
+ spinner.stop();
398
+ console.log(`✅ Template files copied (${filesCopied} files).`);
399
+
400
+ // Remove git history
401
+ const gitDir = path.join(destPath, '.git');
402
+ if (await exists(gitDir)) {
403
+ console.log('Removing .git directory from new site...');
404
+ await fs.rm(gitDir, { recursive: true, force: true });
405
+ }
406
+
407
+ // Pages prompt: show available templates and ask which pages to create (comma-separated)
408
+ if (manifest && Array.isArray(manifest.templates) && manifest.templates.length) {
409
+ printAvailableTemplates(manifest);
410
+ }
411
+ const pagesInput = (await rl.question('Pages to create (comma-separated, e.g. about,contact) [leave blank to skip]: ')).trim();
412
+ let pagesToCreate = [];
413
+ let existingPages = [];
414
+ if (pagesInput) {
415
+ const raw = pagesInput.split(',').map(s => s.trim()).filter(Boolean);
416
+ // sanitize and normalize
417
+ const seen = new Set();
418
+ for (const r of raw) {
419
+ const lower = r.toLowerCase();
420
+ if (lower === 'home' || lower === 'index') {
421
+ existingPages.push('home');
422
+ continue;
423
+ }
424
+ let slug = r.trim().toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
425
+ if (!slug) continue;
426
+ if (seen.has(slug)) continue;
427
+ seen.add(slug);
428
+ const matchedTemplate = findTemplateForSlug(manifest, slug);
429
+ pagesToCreate.push({ slug, displayName: r.trim(), template: matchedTemplate });
430
+ }
431
+
432
+ console.log('\nSummary of pages:');
433
+ if (existingPages.length) console.log(` - Existing (skipped): ${existingPages.join(', ')}`);
434
+ if (pagesToCreate.length) {
435
+ console.log(' - To be created:');
436
+ for (const p of pagesToCreate) {
437
+ if (p.template) {
438
+ console.log(` - ${p.slug} (mapped to template: ${p.template.name})`);
439
+ } else {
440
+ console.log(` - ${p.slug} (no template match)`);
441
+ }
442
+ }
443
+ }
444
+ const proceedPages = (await rl.question('Proceed to create these pages? (Y/n): ')) || 'y';
445
+ if (proceedPages.toLowerCase() === 'y' || proceedPages.toLowerCase() === 'yes') {
446
+ // perform creation
447
+ const templatePagesHome = path.join(templatePath, 'src', 'app', '(pages)', '(home)');
448
+ const siteRoutesFile = path.join(destPath, 'src', 'app', 'data', 'routes.json');
449
+ let routesJson = null;
450
+ try {
451
+ routesJson = JSON.parse(await fs.readFile(siteRoutesFile, 'utf8'));
452
+ // Ensure siteInfo exists and set its name to the root display name
453
+ routesJson.siteInfo = routesJson.siteInfo || {};
454
+ routesJson.siteInfo.name = rootDisplayName;
455
+ } catch (e) {
456
+ console.warn('⚠️ Could not read routes.json, routes will not be updated.');
457
+ }
458
+
459
+ for (const p of pagesToCreate) {
460
+ const targetDir = path.join(destPath, 'src', 'app', '(pages)', p.slug);
461
+ console.log(`Creating page ${p.slug} -> ${targetDir}`);
462
+ let copyResult = null;
463
+ if (p.template && p.template.src) {
464
+ copyResult = await copyTemplateForPage(templatePath, p.template.src, templatePagesHome, targetDir);
465
+ if (copyResult.used === 'template') {
466
+ console.log(` - Copied template ${p.template.name} from ${copyResult.src}`);
467
+ } else {
468
+ console.warn(`⚠️ Template source ${path.join(templatePath, 'src', 'app', '(pages)', path.basename(p.template.src))} not found; using default page template instead.`);
469
+ }
470
+ } else {
471
+ await copyRecursive(templatePagesHome, targetDir);
472
+ }
473
+ // rename component in page.tsx
474
+ const pageFile = path.join(targetDir, 'page.tsx');
475
+ try {
476
+ let content = await fs.readFile(pageFile, 'utf8');
477
+ const compName = p.displayName.replace(/[^a-zA-Z0-9]+/g,' ').split(/\s+/).map(s=>s.charAt(0).toUpperCase()+s.slice(1)).join('') + 'Page';
478
+ content = content.replace(/export default function\s+\w+\s*\(/, `export default function ${compName}(`);
479
+ await fs.writeFile(pageFile, content, 'utf8');
480
+ console.log(` - Updated component name to ${compName} in ${path.relative(destPath, pageFile)}`);
481
+ } catch (e) {
482
+ console.warn(`⚠️ Failed to update component name for ${p.slug}:`, e?.message || e);
483
+ }
484
+
485
+ // update routes.json
486
+ if (routesJson && Array.isArray(routesJson.routes)) {
487
+ // Skip if route path already exists
488
+ const candidatePath = `/${p.slug}`;
489
+ if (!routesJson.routes.some(r => r.path === candidatePath)) {
490
+ routesJson.routes.push({
491
+ "name": p.displayName.split(/\s+/).map(s=>s.charAt(0).toUpperCase()+s.slice(1)).join(' '),
492
+ "path": candidatePath,
493
+ "title": `${rootDisplayName} - ${p.displayName}`,
494
+ "description": "",
495
+ "keywords": ""
496
+ });
497
+ } else {
498
+ console.log(` - Route ${candidatePath} already exists; skipping route add.`);
499
+ }
500
+ }
501
+ }
502
+
503
+ if (manifest) {
504
+ const removed = await pruneTemplateDirs(manifest, destPath, pagesToCreate.map(p=>p.slug));
505
+ for (const r of removed) {
506
+ console.log(`Removed unused template page ${r} from new site...`);
507
+ }
508
+ }
509
+
510
+ if (routesJson) {
511
+ try {
512
+ await fs.writeFile(siteRoutesFile, JSON.stringify(routesJson, null, '\t'), 'utf8');
513
+ console.log('✅ routes.json updated.');
514
+ } catch (e) {
515
+ console.warn('⚠️ Failed to write routes.json:', e?.message || e);
516
+ }
517
+ }
518
+ } else {
519
+ console.log('Skipping page creation.');
520
+ }
521
+ }
522
+ // Automatically replace double-underscore template placeholders (e.g., __SITE_NAME__) with provided values
523
+ const replacements = {};
524
+ if (rootDisplayName) replacements.SITE_NAME = rootDisplayName;
525
+ if (siteUrl) replacements.SITE_URL = siteUrl;
526
+ if (emailAddress) replacements.EMAIL_ADDRESS = emailAddress;
527
+ if (Object.keys(replacements).length) {
528
+ const replArray = [];
529
+ for (const [t, valRaw] of Object.entries(replacements)) {
530
+ const val = String(valRaw);
531
+ // populate TOKEN_MAP so other code can inspect token->value mapping (keyed by literal marker)
532
+ const marker = `__${t}__`;
533
+ TOKEN_MAP[marker] = val;
534
+ // First, replace expression occurrences like {__TOKEN__} with a quoted string expression to avoid bare identifiers
535
+ replArray.push({ tag: `\\{${marker}\\}`, value: `{${JSON.stringify(val)}}`, isRegex: true });
536
+ // Then, replace literal marker occurrences (e.g., __TOKEN__) with the plain value
537
+ replArray.push({ tag: marker, value: val });
538
+ }
539
+ try {
540
+ const changed = await replacePlaceholders(destPath, replArray);
541
+ console.log(`✅ Replaced template placeholders in ${changed} files under ${destPath}`);
542
+ } catch (e) {
543
+ console.warn('⚠️ Failed to replace placeholders in site copy:', e?.message || e);
544
+ }
545
+ }
546
+ // Prompt about creating a new GitHub repository. Default owner is read from components config `github.defaultOwner` (fallback: 'brianwhaley')
547
+ const componentsCfgPath = path.resolve(__dirname, '..', 'config', 'pixelated.config.json');
548
+ let defaultOwner = 'brianwhaley';
549
+ try {
550
+ if (await exists(componentsCfgPath)) {
551
+ const compCfgText = await fs.readFile(componentsCfgPath, 'utf8');
552
+ const compCfg = JSON.parse(compCfgText);
553
+ if (compCfg?.github?.defaultOwner) defaultOwner = compCfg.github.defaultOwner;
554
+ }
555
+ } catch (e) {
556
+ // ignore and use fallback
557
+ }
558
+ const createRemoteAnswer = (await rl.question(`Create a new GitHub repository in '${defaultOwner}' and push the initial commit? (Y/n): `)) || 'y';
559
+ if (createRemoteAnswer.toLowerCase() === 'y' || createRemoteAnswer.toLowerCase() === 'yes') {
560
+ try {
561
+ await createAndPushRemote(destPath, siteName, defaultOwner);
562
+ } catch (e) {
563
+ console.warn('⚠️ Repo creation or git push failed. Your local repository is still available at:', destPath);
564
+ console.warn(e?.stderr || e?.message || e);
565
+ }
566
+ }
567
+
568
+
569
+
570
+ console.log('\n🎉 Done. Summary:');
571
+ console.log(` - Site copied to: ${destPath}`);
572
+ console.log('\nNote: A git remote was not set by this script. You can add one later with `git remote add origin <url>` if desired.');
573
+ console.log('\nNext recommended steps (manual or to be automated in future):');
574
+ console.log(' - Update pixelated.config.json for this site and encrypt it with your config tool');
575
+ console.log(' - Run `npm run lint`, `npm test`, and `npm run build` inside the new site and fix any issues');
576
+ console.log(' - Create GitHub repo (if not already created), push main branch, and set up CI/deploy secrets');
577
+ } catch (err) {
578
+ console.error('Unexpected error:', err);
579
+ process.exit(1);
580
+ } finally {
581
+ rl.close();
582
+ }
583
+ }
584
+
585
+ if (typeof process !== 'undefined' && new URL(import.meta.url).pathname === process.argv[1]) {
586
+ // CLI entry point: run the interactive main flow
587
+ main();
588
+ }
589
+
590
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../src/components/config/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAsB,MAAM,gBAAgB,CAAC;AAO1E;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,eAAe,CA0DxD;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,CAAC,EAAE,eAAe,GAAG,eAAe,CAIpF"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../src/components/config/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAsB,MAAM,gBAAgB,CAAC;AAO1E;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,eAAe,CA4FxD;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,CAAC,EAAE,eAAe,GAAG,eAAe,CAIpF"}
@@ -4,9 +4,12 @@ export function copyTemplateForPage(templatePathArg: any, templateSrc: any, temp
4
4
  used: string;
5
5
  src: any;
6
6
  }>;
7
+ export function createAndPushRemote(destPath: any, siteName: any, defaultOwner: any): Promise<void>;
8
+ export let _exec: typeof execCb.__promisify__;
7
9
  export namespace TOKEN_MAP {
8
10
  let __SITE_NAME__: string;
9
11
  let __SITE_URL__: string;
10
12
  let __EMAIL_ADDRESS__: string;
11
13
  }
14
+ import { exec as execCb } from 'child_process';
12
15
  //# sourceMappingURL=create-pixelated-app.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"create-pixelated-app.d.ts","sourceRoot":"","sources":["../../../src/scripts/create-pixelated-app.js"],"names":[],"mappings":";AA4MA,+GAaC;AAID;;;GAUC"}
1
+ {"version":3,"file":"create-pixelated-app.d.ts","sourceRoot":"","sources":["../../../src/scripts/create-pixelated-app.js"],"names":[],"mappings":";AA8MA,+GAaC;AAID;;;GAUC;AAGD,oGAyGC;AAnQD,8CAAwB;;;;;;+BAZO,eAAe"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=create-pixelated-app.github.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-pixelated-app.github.test.d.ts","sourceRoot":"","sources":["../../../src/tests/create-pixelated-app.github.test.js"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixelated-tech/components",
3
- "version": "3.9.14",
3
+ "version": "3.9.16",
4
4
  "private": false,
5
5
  "author": {
6
6
  "name": "Pixelated Technologies",
@@ -82,7 +82,7 @@
82
82
  "vault": "npx tsx src/scripts/config-vault.ts",
83
83
  "config:vault": "npm run vault --",
84
84
  "config:encrypt": "npm run config:vault -- encrypt src/config/pixelated.config.json",
85
- "config:decrypt": "npm run config:vault -- decrypt src/config/pixelated.config.json",
85
+ "config:decrypt": "npm run config:vault -- decrypt src/config/pixelated.config.json.enc",
86
86
  "create-pixelated-app": "node src/scripts/create-pixelated-app.js"
87
87
  },
88
88
  "scripts-20260113": {
@@ -146,11 +146,11 @@
146
146
  "eslint-plugin-react": "^7.37.4",
147
147
  "eslint-plugin-storybook": "^10.1.11",
148
148
  "file-loader": "^6.2.0",
149
- "happy-dom": "^20.3.0",
149
+ "happy-dom": "^20.3.1",
150
150
  "jsdom": "^27.4.0",
151
151
  "less-loader": "^12.3.0",
152
- "mini-css-extract-plugin": "^2.9.4",
153
- "next": "^16.1.2",
152
+ "mini-css-extract-plugin": "^2.10.0",
153
+ "next": "^16.1.3",
154
154
  "null-loader": "^4.0.1",
155
155
  "prop-types": "^15.8.1",
156
156
  "react": "^19.2.3",
@@ -177,9 +177,9 @@
177
177
  "react-dom": "^19.2.0"
178
178
  },
179
179
  "optionalDependencies": {
180
- "@aws-sdk/client-cloudwatch": "^3.969.0",
181
- "@aws-sdk/client-route-53": "^3.969.0",
182
- "googleapis": "^170.0.0",
180
+ "@aws-sdk/client-cloudwatch": "^3.971.0",
181
+ "@aws-sdk/client-route-53": "^3.971.0",
182
+ "googleapis": "^170.1.0",
183
183
  "md5": "^2.3.0",
184
184
  "puppeteer": "^24.35.0",
185
185
  "react-redux": "*",