@akinon/projectzero 1.116.0-rc.8 → 1.116.0-snapshot-ZERO-3959-20260106131052

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.
@@ -4,7 +4,8 @@ import { useEffect, useRef, useState } from 'react';
4
4
  import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
5
5
  import { closeSearch } from '@akinon/next/redux/reducers/header';
6
6
  import clsx from 'clsx';
7
- import { Icon, Input } from '@theme/components';
7
+
8
+ import { Icon } from '@theme/components';
8
9
  import Results from './results';
9
10
  import { ROUTES } from '@theme/routes';
10
11
  import { useLocalization, useRouter } from '@akinon/next/hooks';
@@ -41,14 +42,6 @@ export default function Search() {
41
42
  };
42
43
  }, [isSearchOpen, dispatch]);
43
44
 
44
- const handleSearchTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
45
- setSearchText(e.target.value);
46
- };
47
-
48
- const handleCloseSearch = () => {
49
- dispatch(closeSearch());
50
- };
51
-
52
45
  return (
53
46
  <>
54
47
  <div
@@ -74,9 +67,9 @@ export default function Search() {
74
67
  {t('common.search.results_for')}
75
68
  </span>
76
69
  <div className="flex items-center">
77
- <Input
70
+ <input
78
71
  value={searchText}
79
- onChange={handleSearchTextChange}
72
+ onChange={(e) => setSearchText(e.target.value)}
80
73
  onKeyDown={(e) => {
81
74
  if (e.key === 'Enter' && searchText.trim() !== '') {
82
75
  router.push(`${ROUTES.LIST}/?search_text=${searchText}`);
@@ -98,12 +91,11 @@ export default function Search() {
98
91
  <Icon
99
92
  name="close"
100
93
  size={14}
101
- onClick={handleCloseSearch}
94
+ onClick={() => dispatch(closeSearch())}
102
95
  className="cursor-pointer"
103
96
  />
104
97
  </div>
105
98
  </div>
106
-
107
99
  <Results searchText={searchText} />
108
100
  </div>
109
101
  </div>
@@ -36,100 +36,53 @@ export default function ProductInfoSlider({ product }: ProductSliderItem) {
36
36
  carouselRef.current?.next();
37
37
  };
38
38
 
39
- const handleThumbnailClick = (index: number) => {
39
+ const handleThumbnailClick = (index) => {
40
40
  setActiveIndex(index);
41
41
  carouselRef.current?.goToSlide(index);
42
42
  };
43
43
 
44
44
  return (
45
- <>
46
- <div className="lg:grid lg:grid-cols-6">
47
- <div className="lg:col-span-1">
48
- <div className="flex flex-col items-center justify-center md:mr-[6px]">
49
- <button
50
- onClick={goToPrev}
51
- className={twMerge(
52
- 'hidden justify-center p-2 mb-3 border border-gray-100 rounded-full cursor-pointer lg:block',
53
- [activeIndex === 0 && 'cursor-not-allowed opacity-45']
54
- )}
55
- disabled={activeIndex === 0}
56
- >
57
- <Icon name="chevron-up" size={15} className="fill-[#000000]" />
58
- </button>
59
- <div className="hidden flex-col items-center overflow-scroll w-[80px] max-h-[620px] lg:block">
60
- {product?.productimage_set?.map((item, index) => (
61
- <Image
62
- key={index}
63
- src={item.image}
64
- alt={`Thumbnail ${index}`}
65
- width={80}
66
- height={128}
67
- aspectRatio={80 / 128}
68
- className={twMerge('cursor-pointer', [
69
- activeIndex === index && 'border-2 border-primary'
70
- ])}
71
- onClick={() => handleThumbnailClick(index)}
72
- />
73
- ))}
74
- </div>
75
- <button
76
- onClick={goToNext}
77
- className={twMerge(
78
- 'hidden justify-center p-2 mt-3 border border-gray-100 rounded-full cursor-pointer lg:block',
79
- [
80
- activeIndex === product.productimage_set.length - 1 &&
81
- 'cursor-not-allowed opacity-45'
82
- ]
83
- )}
84
- disabled={activeIndex === product.productimage_set.length - 1}
85
- >
86
- <Icon name="chevron-down" size={15} className="fill-[#000000]" />
87
- </button>
88
- </div>
89
- </div>
90
-
91
- <div className="relative lg:col-span-5">
92
- <FavButton className="absolute right-8 top-6 z-[20] sm:hidden" />
93
-
94
- <PluginModule
95
- component={Component.ProductImageSearchFeature}
96
- props={{
97
- product,
98
- activeIndex,
99
- showResetButton: true
100
- }}
101
- />
102
-
103
- <CarouselCore
104
- responsive={{
105
- all: {
106
- breakpoint: { max: 5000, min: 0 },
107
- items: 1
108
- }
109
- }}
110
- arrows={false}
111
- swipeable={true}
112
- ref={carouselRef}
113
- afterChange={(previousSlide, { currentSlide }) => {
114
- setActiveIndex(currentSlide);
115
- }}
116
- containerAspectRatio={{ mobile: 520 / 798, desktop: 484 / 726 }}
45
+ <div className="lg:grid lg:grid-cols-6">
46
+ <div className="lg:col-span-1">
47
+ <div className="flex flex-col items-center justify-center md:mr-[6px]">
48
+ <button
49
+ onClick={goToPrev}
50
+ className={twMerge(
51
+ 'hidden justify-center p-2 mb-3 border border-gray-100 rounded-full cursor-pointer lg:block',
52
+ [activeIndex === 0 && 'cursor-not-allowed opacity-45']
53
+ )}
54
+ disabled={activeIndex === 0}
117
55
  >
118
- {product?.productimage_set?.map((item, i) => (
56
+ <Icon name="chevron-up" size={15} className="fill-[#000000]" />
57
+ </button>
58
+ <div className="hidden flex-col items-center overflow-scroll w-[80px] max-h-[620px] lg:block">
59
+ {product?.productimage_set?.map((item, index) => (
119
60
  <Image
120
- key={i}
61
+ key={index}
121
62
  src={item.image}
122
- alt={product?.name || 'Product image'}
123
- draggable={false}
124
- aspectRatio={484 / 726}
125
- sizes="(min-width: 425px) 512px,
126
- (min-width: 601px) 576px,
127
- (min-width: 768px) 336px,
128
- (min-width: 1024px) 484px, 368px"
129
- fill
63
+ alt={`Thumbnail ${index}`}
64
+ width={80}
65
+ height={128}
66
+ className={twMerge('cursor-pointer', [
67
+ activeIndex === index && 'border-2 border-primary'
68
+ ])}
69
+ onClick={() => handleThumbnailClick(index)}
130
70
  />
131
71
  ))}
132
- </CarouselCore>
72
+ </div>
73
+ <button
74
+ onClick={goToNext}
75
+ className={twMerge(
76
+ 'hidden justify-center p-2 mt-3 border border-gray-100 rounded-full cursor-pointer lg:block',
77
+ [
78
+ activeIndex === product.productimage_set.length - 1 &&
79
+ 'cursor-not-allowed opacity-45'
80
+ ]
81
+ )}
82
+ disabled={activeIndex === product.productimage_set.length - 1}
83
+ >
84
+ <Icon name="chevron-down" size={15} className="fill-[#000000]" />
85
+ </button>
133
86
  </div>
134
87
  </div>
135
88
 
@@ -137,10 +137,6 @@ export default async () => {
137
137
  name: 'Tamara Payment Extension',
138
138
  value: 'pz-tamara-extension'
139
139
  },
140
- {
141
- name: 'Similar Products',
142
- value: 'pz-similar-products'
143
- },
144
140
  {
145
141
  name: 'Hepsipay',
146
142
  value: 'pz-hepsipay'
@@ -1,208 +1,274 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || function (mod) {
1
+ 'use strict';
2
+ var __createBinding =
3
+ (this && this.__createBinding) ||
4
+ (Object.create
5
+ ? function (o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (
9
+ !desc ||
10
+ ('get' in desc ? !m.__esModule : desc.writable || desc.configurable)
11
+ ) {
12
+ desc = {
13
+ enumerable: true,
14
+ get: function () {
15
+ return m[k];
16
+ }
17
+ };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }
21
+ : function (o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ o[k2] = m[k];
24
+ });
25
+ var __setModuleDefault =
26
+ (this && this.__setModuleDefault) ||
27
+ (Object.create
28
+ ? function (o, v) {
29
+ Object.defineProperty(o, 'default', { enumerable: true, value: v });
30
+ }
31
+ : function (o, v) {
32
+ o['default'] = v;
33
+ });
34
+ var __importStar =
35
+ (this && this.__importStar) ||
36
+ function (mod) {
19
37
  if (mod && mod.__esModule) return mod;
20
38
  var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
39
+ if (mod != null)
40
+ for (var k in mod)
41
+ if (k !== 'default' && Object.prototype.hasOwnProperty.call(mod, k))
42
+ __createBinding(result, mod, k);
22
43
  __setModuleDefault(result, mod);
23
44
  return result;
24
- };
25
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
45
+ };
46
+ var __awaiter =
47
+ (this && this.__awaiter) ||
48
+ function (thisArg, _arguments, P, generator) {
49
+ function adopt(value) {
50
+ return value instanceof P
51
+ ? value
52
+ : new P(function (resolve) {
53
+ resolve(value);
54
+ });
55
+ }
27
56
  return new (P || (P = Promise))(function (resolve, reject) {
28
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
- step((generator = generator.apply(thisArg, _arguments || [])).next());
32
- });
33
- };
34
- var __importDefault = (this && this.__importDefault) || function (mod) {
35
- return (mod && mod.__esModule) ? mod : { "default": mod };
36
- };
37
- Object.defineProperty(exports, "__esModule", { value: true });
38
- const fs = __importStar(require("fs"));
39
- const path_1 = __importDefault(require("path"));
40
- const child_process_1 = require("child_process");
41
- const semver_1 = __importDefault(require("semver"));
42
- const prompts_1 = require("@inquirer/prompts");
43
- const rootDir = path_1.default.resolve(process.cwd());
44
- function checkVersion(pkg) {
45
- return __awaiter(this, void 0, void 0, function* () {
46
- const packageName = '@akinon/next';
47
- const registryUrl = `https://registry.npmjs.org/${packageName}`;
57
+ function fulfilled(value) {
48
58
  try {
49
- const response = yield fetch(registryUrl);
50
- const pkgInfo = (yield response.json());
51
- const latestVersion = pkgInfo['dist-tags'].latest;
52
- if (!semver_1.default.satisfies(pkg.dependencies['@akinon/next'], latestVersion)) {
53
- console.warn(`\x1b[43mWarning: The "${packageName}" package is currently at version ${pkg.dependencies['@akinon/next']}. Please upgrade it to the latest version (${latestVersion}) to ensure plugin compatibility.`, '\x1b[0m\n');
54
- }
59
+ step(generator.next(value));
60
+ } catch (e) {
61
+ reject(e);
55
62
  }
56
- catch (error) {
57
- console.error(`\x1b[41mError: ${error === null || error === void 0 ? void 0 : error.message}`, '\x1b[0m\n');
63
+ }
64
+ function rejected(value) {
65
+ try {
66
+ step(generator['throw'](value));
67
+ } catch (e) {
68
+ reject(e);
58
69
  }
70
+ }
71
+ function step(result) {
72
+ result.done
73
+ ? resolve(result.value)
74
+ : adopt(result.value).then(fulfilled, rejected);
75
+ }
76
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
59
77
  });
78
+ };
79
+ var __importDefault =
80
+ (this && this.__importDefault) ||
81
+ function (mod) {
82
+ return mod && mod.__esModule ? mod : { default: mod };
83
+ };
84
+ Object.defineProperty(exports, '__esModule', { value: true });
85
+ const fs = __importStar(require('fs'));
86
+ const path_1 = __importDefault(require('path'));
87
+ const child_process_1 = require('child_process');
88
+ const semver_1 = __importDefault(require('semver'));
89
+ const prompts_1 = require('@inquirer/prompts');
90
+ const rootDir = path_1.default.resolve(process.cwd());
91
+ function checkVersion(pkg) {
92
+ return __awaiter(this, void 0, void 0, function* () {
93
+ const packageName = '@akinon/next';
94
+ const registryUrl = `https://registry.npmjs.org/${packageName}`;
95
+ try {
96
+ const response = yield fetch(registryUrl);
97
+ const pkgInfo = yield response.json();
98
+ const latestVersion = pkgInfo['dist-tags'].latest;
99
+ if (
100
+ !semver_1.default.satisfies(
101
+ pkg.dependencies['@akinon/next'],
102
+ latestVersion
103
+ )
104
+ ) {
105
+ console.warn(
106
+ `\x1b[43mWarning: The "${packageName}" package is currently at version ${pkg.dependencies['@akinon/next']}. Please upgrade it to the latest version (${latestVersion}) to ensure plugin compatibility.`,
107
+ '\x1b[0m\n'
108
+ );
109
+ }
110
+ } catch (error) {
111
+ console.error(
112
+ `\x1b[41mError: ${
113
+ error === null || error === void 0 ? void 0 : error.message
114
+ }`,
115
+ '\x1b[0m\n'
116
+ );
117
+ }
118
+ });
60
119
  }
61
- exports.default = () => __awaiter(void 0, void 0, void 0, function* () {
120
+ exports.default = () =>
121
+ __awaiter(void 0, void 0, void 0, function* () {
62
122
  function findPluginsFilePath() {
63
- const option1 = path_1.default.resolve(rootDir, './src/plugins.js');
64
- const option2 = path_1.default.resolve(rootDir, './packages/akinon-next/plugins.js');
65
- if (fs.existsSync(option1)) {
66
- return option1;
67
- }
68
- else if (fs.existsSync(option2)) {
69
- return option2;
70
- }
71
- else {
72
- throw new Error('plugins.js was not found in either of the expected locations.');
73
- }
123
+ const option1 = path_1.default.resolve(rootDir, './src/plugins.js');
124
+ const option2 = path_1.default.resolve(
125
+ rootDir,
126
+ './packages/akinon-next/plugins.js'
127
+ );
128
+ if (fs.existsSync(option1)) {
129
+ return option1;
130
+ } else if (fs.existsSync(option2)) {
131
+ return option2;
132
+ } else {
133
+ throw new Error(
134
+ 'plugins.js was not found in either of the expected locations.'
135
+ );
136
+ }
74
137
  }
75
138
  const pkg = require(path_1.default.resolve(rootDir, './package.json'));
76
139
  yield checkVersion(pkg);
77
140
  const pluginsFilePath = findPluginsFilePath();
78
141
  let installedPlugins = [];
79
142
  try {
80
- installedPlugins = require(pluginsFilePath);
81
- }
82
- catch (error) {
83
- console.error('Error loading installed plugins:', error);
84
- process.exit(1);
143
+ installedPlugins = require(pluginsFilePath);
144
+ } catch (error) {
145
+ console.error('Error loading installed plugins:', error);
146
+ process.exit(1);
85
147
  }
86
148
  const definedPlugins = [
87
- {
88
- name: 'Akifast',
89
- value: 'pz-akifast'
90
- },
91
- {
92
- name: 'Apple Pay',
93
- value: 'pz-apple-pay'
94
- },
95
- {
96
- name: 'B2B',
97
- value: 'pz-b2b'
98
- },
99
- {
100
- name: 'Basket Gift Pack',
101
- value: 'pz-basket-gift-pack'
102
- },
103
- {
104
- name: 'BKM Express',
105
- value: 'pz-bkm'
106
- },
107
- {
108
- name: 'Checkout Gift Pack',
109
- value: 'pz-checkout-gift-pack'
110
- },
111
- {
112
- name: 'Click & Collect',
113
- value: 'pz-click-collect'
114
- },
115
- {
116
- name: 'Credit Payment',
117
- value: 'pz-credit-payment'
118
- },
119
- {
120
- name: 'Garanti Pay',
121
- value: 'pz-gpay'
122
- },
123
- {
124
- name: 'Masterpass',
125
- value: 'pz-masterpass'
126
- },
127
- {
128
- name: 'Multi Basket',
129
- value: 'pz-multi-basket'
130
- },
131
- {
132
- name: 'One Click Checkout',
133
- value: 'pz-one-click-checkout'
134
- },
135
- {
136
- name: 'Otp',
137
- value: 'pz-otp'
138
- },
139
- {
140
- name: 'Pay On Delivery',
141
- value: 'pz-pay-on-delivery'
142
- },
143
- {
144
- name: 'Saved Card',
145
- value: 'pz-saved-card'
146
- },
147
- {
148
- name: 'Tabby Payment Extension',
149
- value: 'pz-tabby-extension'
150
- },
151
- {
152
- name: 'Tamara Payment Extension',
153
- value: 'pz-tamara-extension'
154
- },
155
- {
156
- name: 'Similar Products',
157
- value: 'pz-similar-products'
158
- },
159
- {
160
- name: 'Hepsipay',
161
- value: 'pz-hepsipay'
162
- },
163
- {
164
- name: 'Flow Payment',
165
- value: 'pz-flow-payment'
166
- },
167
- {
168
- name: 'Virtual Try-On',
169
- value: 'pz-virtual-try-on'
170
- },
171
- {
172
- name: 'Masterpass Rest',
173
- value: 'pz-masterpass-rest'
174
- },
175
- {
176
- name: 'Similar Products',
177
- value: 'pz-similar-products'
178
- },
179
- {
180
- name: 'Haso Payment Gateway',
181
- value: 'pz-haso'
182
- }
149
+ {
150
+ name: 'Akifast',
151
+ value: 'pz-akifast'
152
+ },
153
+ {
154
+ name: 'Apple Pay',
155
+ value: 'pz-apple-pay'
156
+ },
157
+ {
158
+ name: 'B2B',
159
+ value: 'pz-b2b'
160
+ },
161
+ {
162
+ name: 'Basket Gift Pack',
163
+ value: 'pz-basket-gift-pack'
164
+ },
165
+ {
166
+ name: 'BKM Express',
167
+ value: 'pz-bkm'
168
+ },
169
+ {
170
+ name: 'Checkout Gift Pack',
171
+ value: 'pz-checkout-gift-pack'
172
+ },
173
+ {
174
+ name: 'Click & Collect',
175
+ value: 'pz-click-collect'
176
+ },
177
+ {
178
+ name: 'Credit Payment',
179
+ value: 'pz-credit-payment'
180
+ },
181
+ {
182
+ name: 'Garanti Pay',
183
+ value: 'pz-gpay'
184
+ },
185
+ {
186
+ name: 'Masterpass',
187
+ value: 'pz-masterpass'
188
+ },
189
+ {
190
+ name: 'Multi Basket',
191
+ value: 'pz-multi-basket'
192
+ },
193
+ {
194
+ name: 'One Click Checkout',
195
+ value: 'pz-one-click-checkout'
196
+ },
197
+ {
198
+ name: 'Otp',
199
+ value: 'pz-otp'
200
+ },
201
+ {
202
+ name: 'Pay On Delivery',
203
+ value: 'pz-pay-on-delivery'
204
+ },
205
+ {
206
+ name: 'Saved Card',
207
+ value: 'pz-saved-card'
208
+ },
209
+ {
210
+ name: 'Tabby Payment Extension',
211
+ value: 'pz-tabby-extension'
212
+ },
213
+ {
214
+ name: 'Tamara Payment Extension',
215
+ value: 'pz-tamara-extension'
216
+ },
217
+ {
218
+ name: 'Hepsipay',
219
+ value: 'pz-hepsipay'
220
+ },
221
+ {
222
+ name: 'Flow Payment',
223
+ value: 'pz-flow-payment'
224
+ },
225
+ {
226
+ name: 'Virtual Try-On',
227
+ value: 'pz-virtual-try-on'
228
+ },
229
+ {
230
+ name: 'Masterpass Rest',
231
+ value: 'pz-masterpass-rest'
232
+ },
233
+ {
234
+ name: 'Similar Products',
235
+ value: 'pz-similar-products'
236
+ },
237
+ {
238
+ name: 'Haso Payment Gateway',
239
+ value: 'pz-haso'
240
+ }
183
241
  ];
184
242
  try {
185
- const answers = yield (0, prompts_1.checkbox)({
186
- message: 'Please check/uncheck plugins to install/uninstall.',
187
- choices: definedPlugins.map((plugin) => ({
188
- name: plugin.name,
189
- value: plugin.value,
190
- checked: installedPlugins.includes(plugin.value)
191
- }))
192
- });
193
- if (!answers.length) {
194
- console.log('\x1b[33m%s\x1b[0m', `\nUninstalling all plugins.`);
243
+ const answers = yield (0, prompts_1.checkbox)({
244
+ message: 'Please check/uncheck plugins to install/uninstall.',
245
+ choices: definedPlugins.map((plugin) => ({
246
+ name: plugin.name,
247
+ value: plugin.value,
248
+ checked: installedPlugins.includes(plugin.value)
249
+ }))
250
+ });
251
+ if (!answers.length) {
252
+ console.log('\x1b[33m%s\x1b[0m', `\nUninstalling all plugins.`);
253
+ }
254
+ console.log('\x1b[36m%s\x1b[0m', `\nPlease wait...`);
255
+ fs.writeFileSync(
256
+ pluginsFilePath,
257
+ `module.exports = ${JSON.stringify(answers)};\n`,
258
+ {
259
+ encoding: 'utf-8'
195
260
  }
196
- console.log('\x1b[36m%s\x1b[0m', `\nPlease wait...`);
197
- fs.writeFileSync(pluginsFilePath, `module.exports = ${JSON.stringify(answers)};\n`, {
198
- encoding: 'utf-8'
199
- });
200
- (0, child_process_1.execSync)('yarn install', { stdio: 'pipe' });
201
- console.log('\x1b[32m%s\x1b[0m', `\n✓ ${answers.length
261
+ );
262
+ (0, child_process_1.execSync)('yarn install', { stdio: 'pipe' });
263
+ console.log(
264
+ '\x1b[32m%s\x1b[0m',
265
+ `\n✓ ${
266
+ answers.length
202
267
  ? 'Installed selected plugins'
203
- : 'Uninstalled all plugins'}.\n`);
204
- }
205
- catch (error) {
206
- process.exit(1);
268
+ : 'Uninstalled all plugins'
269
+ }.\n`
270
+ );
271
+ } catch (error) {
272
+ process.exit(1);
207
273
  }
208
- });
274
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinon/projectzero",
3
- "version": "1.116.0-rc.8",
3
+ "version": "1.116.0-snapshot-ZERO-3959-20260106131052",
4
4
  "private": false,
5
5
  "description": "CLI tool to manage your Project Zero Next project",
6
6
  "bin": {