@akropolys/cli 1.5.3

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.
package/dist/index.js ADDED
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/index.ts
27
+ var import_commander = require("commander");
28
+
29
+ // src/commands/init.ts
30
+ var import_readline = __toESM(require("readline"));
31
+ var import_fs = __toESM(require("fs"));
32
+ var import_picocolors = __toESM(require("picocolors"));
33
+ var question = (rl, query) => {
34
+ return new Promise((resolve) => rl.question(query, resolve));
35
+ };
36
+ async function runInit() {
37
+ console.log(import_picocolors.default.bold(import_picocolors.default.cyan("\nConfiguring Akropolys Workspace...")));
38
+ console.log(import_picocolors.default.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
39
+ const rl = import_readline.default.createInterface({
40
+ input: process.stdin,
41
+ output: process.stdout
42
+ });
43
+ try {
44
+ const siteId = await question(rl, import_picocolors.default.cyan("? Enter your Akropolys Site ID: "));
45
+ const apiToken = await question(rl, import_picocolors.default.cyan("? Enter your Akropolys API Token: "));
46
+ const apiUrlInput = await question(rl, import_picocolors.default.cyan("? Enter your Akropolys API URL (default: https://api.akropolys.io): "));
47
+ const apiUrl = apiUrlInput.trim() || "https://api.akropolys.io";
48
+ console.log(import_picocolors.default.cyan("? Select your vertical:"));
49
+ console.log(" 1. commerce");
50
+ console.log(" 2. property");
51
+ console.log(" 3. motor");
52
+ const verticalIndex = await question(rl, import_picocolors.default.cyan(" Select (1-3, default: 1): "));
53
+ let vertical = "commerce";
54
+ if (verticalIndex === "2") vertical = "property";
55
+ else if (verticalIndex === "3") vertical = "motor";
56
+ const envContent = `NEXT_PUBLIC_AKROPOLYS_SITE_ID=${siteId.trim()}
57
+ NEXT_PUBLIC_AKROPOLYS_API_TOKEN=${apiToken.trim()}
58
+ NEXT_PUBLIC_AKROPOLYS_API_URL=${apiUrl.trim()}
59
+ NEXT_PUBLIC_AKROPOLYS_VERTICAL=${vertical}
60
+ `;
61
+ import_fs.default.writeFileSync(".env", envContent, "utf-8");
62
+ console.log(import_picocolors.default.dim("\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
63
+ console.log(import_picocolors.default.green("\u2713 Generated .env with configuration parameters."));
64
+ console.log(import_picocolors.default.green("\u2713 Saved config template."));
65
+ } catch (err) {
66
+ console.error(import_picocolors.default.red(`
67
+ \u274C Error generating configuration: ${err.message}`));
68
+ process.exit(1);
69
+ } finally {
70
+ rl.close();
71
+ }
72
+ }
73
+
74
+ // src/commands/doctor.ts
75
+ var import_fs2 = __toESM(require("fs"));
76
+ var import_picocolors2 = __toESM(require("picocolors"));
77
+ function loadEnv() {
78
+ const envPaths = [".env", ".env.local"];
79
+ for (const envPath of envPaths) {
80
+ if (import_fs2.default.existsSync(envPath)) {
81
+ const content = import_fs2.default.readFileSync(envPath, "utf-8");
82
+ const lines = content.split(/\r?\n/);
83
+ for (const line of lines) {
84
+ const trimmed = line.trim();
85
+ if (!trimmed || trimmed.startsWith("#")) continue;
86
+ const eqIdx = trimmed.indexOf("=");
87
+ if (eqIdx > 0) {
88
+ const key = trimmed.substring(0, eqIdx).trim();
89
+ const value = trimmed.substring(eqIdx + 1).trim().replace(/^['"]|['"]$/g, "");
90
+ if (key) {
91
+ process.env[key] = value;
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ async function runDoctor(options) {
99
+ console.log(import_picocolors2.default.bold("\nAkropolys Doctor"));
100
+ console.log(import_picocolors2.default.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
101
+ loadEnv();
102
+ const siteId = process.env.NEXT_PUBLIC_AKROPOLYS_SITE_ID || process.env.VITE_AKROPOLYS_SITE_ID || "";
103
+ const apiToken = process.env.NEXT_PUBLIC_AKROPOLYS_API_TOKEN || process.env.VITE_AKROPOLYS_API_TOKEN || "";
104
+ const apiUrl = process.env.NEXT_PUBLIC_AKROPOLYS_API_URL || process.env.VITE_AKROPOLYS_API_URL || "https://api.akropolys.io";
105
+ const vertical = process.env.NEXT_PUBLIC_AKROPOLYS_VERTICAL || process.env.VITE_AKROPOLYS_VERTICAL || "commerce";
106
+ if (options.verbose) {
107
+ console.log(import_picocolors2.default.dim(`[Verbose] Site ID: ${siteId || "<not set>"}`));
108
+ console.log(import_picocolors2.default.dim(`[Verbose] API Token: ${apiToken ? "********" : "<not set>"}`));
109
+ console.log(import_picocolors2.default.dim(`[Verbose] API URL: ${apiUrl}`));
110
+ console.log(import_picocolors2.default.dim(`[Verbose] Vertical: ${vertical}`));
111
+ }
112
+ if (!siteId) {
113
+ console.log(import_picocolors2.default.red("\u274C Configuration: Site ID is missing. Set NEXT_PUBLIC_AKROPOLYS_SITE_ID in your env."));
114
+ process.exit(1);
115
+ }
116
+ console.log(import_picocolors2.default.green(`\u2713 Configuration: Site ID detected (${siteId})`));
117
+ if (!apiToken) {
118
+ console.log(import_picocolors2.default.red("\u274C Environment: API Token is missing. Set NEXT_PUBLIC_AKROPOLYS_API_TOKEN in your env."));
119
+ process.exit(1);
120
+ }
121
+ console.log(import_picocolors2.default.green("\u2713 Environment: API Token detected"));
122
+ const start = Date.now();
123
+ try {
124
+ const res = await fetch(`${apiUrl}/health`, {
125
+ method: "GET",
126
+ headers: {
127
+ "X-Akropolys-Token": apiToken,
128
+ "X-Akropolys-Site": siteId
129
+ }
130
+ }).catch((err) => {
131
+ throw new Error(`Fetch failed: ${err.message}`);
132
+ });
133
+ const duration = Date.now() - start;
134
+ if (!res.ok) {
135
+ console.log(import_picocolors2.default.red(`\u274C Connection: API responded with status ${res.status} (Ping: ${duration}ms)`));
136
+ process.exit(2);
137
+ }
138
+ console.log(import_picocolors2.default.green(`\u2713 Connection: Successfully connected to ${apiUrl} (ping: ${duration}ms)`));
139
+ console.log(import_picocolors2.default.green(`\u2713 Integration: Site vertical configured as "${vertical}"`));
140
+ console.log(import_picocolors2.default.bold(import_picocolors2.default.green("\nStatus: Healthy (All configuration and connectivity checks passed)")));
141
+ process.exit(0);
142
+ } catch (err) {
143
+ console.log(import_picocolors2.default.red(`\u274C Connection: Unreachable API at ${apiUrl}. Error: ${err.message}`));
144
+ process.exit(2);
145
+ }
146
+ }
147
+
148
+ // src/commands/inspect.ts
149
+ var import_fs3 = __toESM(require("fs"));
150
+ var import_picocolors3 = __toESM(require("picocolors"));
151
+ function readStdin() {
152
+ return new Promise((resolve, reject) => {
153
+ let data = "";
154
+ process.stdin.setEncoding("utf-8");
155
+ process.stdin.on("readable", () => {
156
+ let chunk;
157
+ while ((chunk = process.stdin.read()) !== null) {
158
+ data += chunk;
159
+ }
160
+ });
161
+ process.stdin.on("end", () => {
162
+ resolve(data);
163
+ });
164
+ process.stdin.on("error", (err) => {
165
+ reject(err);
166
+ });
167
+ });
168
+ }
169
+ async function runInspect(filePath, options) {
170
+ console.log(import_picocolors3.default.bold("\nAkropolys Inspect"));
171
+ console.log(import_picocolors3.default.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
172
+ let rawData = "";
173
+ try {
174
+ if (options.stdin || !filePath && !process.stdin.isTTY) {
175
+ rawData = await readStdin();
176
+ } else if (filePath) {
177
+ if (!import_fs3.default.existsSync(filePath)) {
178
+ console.error(import_picocolors3.default.red(`\u274C Error: File not found at "${filePath}"`));
179
+ process.exit(1);
180
+ }
181
+ rawData = import_fs3.default.readFileSync(filePath, "utf-8");
182
+ } else {
183
+ console.error(import_picocolors3.default.red("\u274C Error: Provide a file path or pipe via standard input using --stdin"));
184
+ process.exit(1);
185
+ }
186
+ } catch (err) {
187
+ console.error(import_picocolors3.default.red(`\u274C Error reading input: ${err.message}`));
188
+ process.exit(1);
189
+ }
190
+ let items = [];
191
+ try {
192
+ const parsed = JSON.parse(rawData.trim());
193
+ items = Array.isArray(parsed) ? parsed : [parsed];
194
+ } catch (err) {
195
+ console.error(import_picocolors3.default.red(`\u274C Error parsing JSON: Invalid JSON structure. ${err.message}`));
196
+ process.exit(1);
197
+ }
198
+ console.log(import_picocolors3.default.cyan(`Parsed ${items.length} catalog items.`));
199
+ console.log(import_picocolors3.default.dim("\nIngestion Quality Diagnostics:"));
200
+ let warningsCount = 0;
201
+ items.forEach((item, index) => {
202
+ const identifier = item.id || item.productId || item.slug || item.url || item.name || `item at index ${index}`;
203
+ const hasId = item.id !== void 0 && item.id !== null && item.id !== "";
204
+ const hasProductId = item.productId !== void 0 && item.productId !== null && item.productId !== "";
205
+ const hasSlug = item.slug !== void 0 && item.slug !== null && item.slug !== "";
206
+ const hasUrl = item.url !== void 0 && item.url !== null && item.url !== "";
207
+ const hasName = item.name !== void 0 && item.name !== null && item.name !== "";
208
+ if (!hasId && !hasProductId && !hasSlug && !hasUrl && !hasName) {
209
+ console.log(` ${import_picocolors3.default.yellow("\u26A0")} [AP001] Missing Stable Identifier: "${identifier}" \u2794 Deduplication & correlation unavailable`);
210
+ warningsCount++;
211
+ }
212
+ const keysCount = Object.keys(item).length;
213
+ if (keysCount < 2) {
214
+ console.log(` ${import_picocolors3.default.yellow("\u26A0")} [AP002] Low-Signal Payload: "${identifier}" has sparse attributes \u2794 Search vector quality reduced`);
215
+ warningsCount++;
216
+ }
217
+ });
218
+ console.log(import_picocolors3.default.dim("\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
219
+ console.log(import_picocolors3.default.bold(`Inspect complete: ${items.length} items checked, ${warningsCount} warnings flagged.`));
220
+ if (warningsCount > 0 && options.strict) {
221
+ console.log(import_picocolors3.default.red("Exit code 3: strict mode failed due to warnings."));
222
+ process.exit(3);
223
+ }
224
+ process.exit(0);
225
+ }
226
+
227
+ // src/index.ts
228
+ var program = new import_commander.Command();
229
+ program.name("akropolys").description("Akropolys Command Line Tool \u2014 Developer diagnostics, setup helper, and structural inspector.").version("1.0.0");
230
+ program.command("init").description("Configure the local workspace by generating a default .env file template.").action(async () => {
231
+ await runInit();
232
+ });
233
+ program.command("doctor").description("Perform a health check consolidating local configuration values and backend API reachability.").option("-v, --verbose", "Include verbose logs and configuration dumps.").action(async (options) => {
234
+ await runDoctor(options);
235
+ });
236
+ program.command("inspect").description("Statically inspect a catalog payload file or stream against the Akropolys Anti-Pattern Registry.").argument("[file]", "Path to the local catalog JSON file.").option("--stdin", "Force reading data from standard input.").option("--strict", "Fail the process (exit code 3) if any structural registry warnings are triggered.").action(async (file, options) => {
237
+ await runInspect(file, options);
238
+ });
239
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@akropolys/cli",
3
+ "version": "1.5.3",
4
+ "description": "Akropolys CLI — diagnostic, workspace setup, and payload inspection utility.",
5
+ "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "akropolys": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsup",
12
+ "dev": "tsup --watch"
13
+ },
14
+ "dependencies": {
15
+ "commander": "^12.0.0",
16
+ "picocolors": "^1.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "tsup": "^8.0.0",
20
+ "typescript": "^5.0.0",
21
+ "@types/node": "^20.0.0"
22
+ }
23
+ }
@@ -0,0 +1,87 @@
1
+ import fs from 'fs';
2
+ import pc from 'picocolors';
3
+
4
+ export function loadEnv() {
5
+ const envPaths = ['.env', '.env.local'];
6
+ for (const envPath of envPaths) {
7
+ if (fs.existsSync(envPath)) {
8
+ const content = fs.readFileSync(envPath, 'utf-8');
9
+ const lines = content.split(/\r?\n/);
10
+ for (const line of lines) {
11
+ const trimmed = line.trim();
12
+ if (!trimmed || trimmed.startsWith('#')) continue;
13
+ const eqIdx = trimmed.indexOf('=');
14
+ if (eqIdx > 0) {
15
+ const key = trimmed.substring(0, eqIdx).trim();
16
+ const value = trimmed.substring(eqIdx + 1).trim().replace(/^['"]|['"]$/g, '');
17
+ if (key) {
18
+ process.env[key] = value;
19
+ }
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
25
+
26
+ export async function runDoctor(options: { verbose?: boolean }) {
27
+ console.log(pc.bold('\nAkropolys Doctor'));
28
+ console.log(pc.dim('─────────────────────────'));
29
+
30
+ loadEnv();
31
+
32
+ const siteId = process.env.NEXT_PUBLIC_AKROPOLYS_SITE_ID || process.env.VITE_AKROPOLYS_SITE_ID || '';
33
+ const apiToken = process.env.NEXT_PUBLIC_AKROPOLYS_API_TOKEN || process.env.VITE_AKROPOLYS_API_TOKEN || '';
34
+ const apiUrl = process.env.NEXT_PUBLIC_AKROPOLYS_API_URL || process.env.VITE_AKROPOLYS_API_URL || 'https://api.akropolys.io';
35
+ const vertical = process.env.NEXT_PUBLIC_AKROPOLYS_VERTICAL || process.env.VITE_AKROPOLYS_VERTICAL || 'commerce';
36
+
37
+ if (options.verbose) {
38
+ console.log(pc.dim(`[Verbose] Site ID: ${siteId || '<not set>'}`));
39
+ console.log(pc.dim(`[Verbose] API Token: ${apiToken ? '********' : '<not set>'}`));
40
+ console.log(pc.dim(`[Verbose] API URL: ${apiUrl}`));
41
+ console.log(pc.dim(`[Verbose] Vertical: ${vertical}`));
42
+ }
43
+
44
+ // 1. Configuration check
45
+ if (!siteId) {
46
+ console.log(pc.red('❌ Configuration: Site ID is missing. Set NEXT_PUBLIC_AKROPOLYS_SITE_ID in your env.'));
47
+ process.exit(1);
48
+ }
49
+ console.log(pc.green(`✓ Configuration: Site ID detected (${siteId})`));
50
+
51
+ if (!apiToken) {
52
+ console.log(pc.red('❌ Environment: API Token is missing. Set NEXT_PUBLIC_AKROPOLYS_API_TOKEN in your env.'));
53
+ process.exit(1);
54
+ }
55
+ console.log(pc.green('✓ Environment: API Token detected'));
56
+
57
+ // 2. Connectivity check
58
+ const start = Date.now();
59
+ try {
60
+ const res = await fetch(`${apiUrl}/health`, {
61
+ method: 'GET',
62
+ headers: {
63
+ 'X-Akropolys-Token': apiToken,
64
+ 'X-Akropolys-Site': siteId,
65
+ },
66
+ }).catch(err => {
67
+ // Throw fetch failures
68
+ throw new Error(`Fetch failed: ${err.message}`);
69
+ });
70
+
71
+ const duration = Date.now() - start;
72
+
73
+ if (!res.ok) {
74
+ console.log(pc.red(`❌ Connection: API responded with status ${res.status} (Ping: ${duration}ms)`));
75
+ process.exit(2);
76
+ }
77
+
78
+ console.log(pc.green(`✓ Connection: Successfully connected to ${apiUrl} (ping: ${duration}ms)`));
79
+ console.log(pc.green(`✓ Integration: Site vertical configured as "${vertical}"`));
80
+
81
+ console.log(pc.bold(pc.green('\nStatus: Healthy (All configuration and connectivity checks passed)')));
82
+ process.exit(0);
83
+ } catch (err: any) {
84
+ console.log(pc.red(`❌ Connection: Unreachable API at ${apiUrl}. Error: ${err.message}`));
85
+ process.exit(2);
86
+ }
87
+ }
@@ -0,0 +1,49 @@
1
+ import readline from 'readline';
2
+ import fs from 'fs';
3
+ import pc from 'picocolors';
4
+
5
+ const question = (rl: readline.Interface, query: string): Promise<string> => {
6
+ return new Promise((resolve) => rl.question(query, resolve));
7
+ };
8
+
9
+ export async function runInit() {
10
+ console.log(pc.bold(pc.cyan('\nConfiguring Akropolys Workspace...')));
11
+ console.log(pc.dim('─────────────────────────'));
12
+
13
+ const rl = readline.createInterface({
14
+ input: process.stdin,
15
+ output: process.stdout,
16
+ });
17
+
18
+ try {
19
+ const siteId = await question(rl, pc.cyan('? Enter your Akropolys Site ID: '));
20
+ const apiToken = await question(rl, pc.cyan('? Enter your Akropolys API Token: '));
21
+ const apiUrlInput = await question(rl, pc.cyan('? Enter your Akropolys API URL (default: https://api.akropolys.io): '));
22
+ const apiUrl = apiUrlInput.trim() || 'https://api.akropolys.io';
23
+
24
+ console.log(pc.cyan('? Select your vertical:'));
25
+ console.log(' 1. commerce');
26
+ console.log(' 2. property');
27
+ console.log(' 3. motor');
28
+ const verticalIndex = await question(rl, pc.cyan(' Select (1-3, default: 1): '));
29
+ let vertical = 'commerce';
30
+ if (verticalIndex === '2') vertical = 'property';
31
+ else if (verticalIndex === '3') vertical = 'motor';
32
+
33
+ const envContent = `NEXT_PUBLIC_AKROPOLYS_SITE_ID=${siteId.trim()}
34
+ NEXT_PUBLIC_AKROPOLYS_API_TOKEN=${apiToken.trim()}
35
+ NEXT_PUBLIC_AKROPOLYS_API_URL=${apiUrl.trim()}
36
+ NEXT_PUBLIC_AKROPOLYS_VERTICAL=${vertical}
37
+ `;
38
+
39
+ fs.writeFileSync('.env', envContent, 'utf-8');
40
+ console.log(pc.dim('\n─────────────────────────'));
41
+ console.log(pc.green('✓ Generated .env with configuration parameters.'));
42
+ console.log(pc.green('✓ Saved config template.'));
43
+ } catch (err: any) {
44
+ console.error(pc.red(`\n❌ Error generating configuration: ${err.message}`));
45
+ process.exit(1);
46
+ } finally {
47
+ rl.close();
48
+ }
49
+ }
@@ -0,0 +1,100 @@
1
+ import fs from 'fs';
2
+ import pc from 'picocolors';
3
+
4
+ function readStdin(): Promise<string> {
5
+ return new Promise((resolve, reject) => {
6
+ let data = '';
7
+ process.stdin.setEncoding('utf-8');
8
+ process.stdin.on('readable', () => {
9
+ let chunk;
10
+ while ((chunk = process.stdin.read()) !== null) {
11
+ data += chunk;
12
+ }
13
+ });
14
+ process.stdin.on('end', () => {
15
+ resolve(data);
16
+ });
17
+ process.stdin.on('error', (err) => {
18
+ reject(err);
19
+ });
20
+ });
21
+ }
22
+
23
+ export async function runInspect(
24
+ filePath: string | undefined,
25
+ options: { stdin?: boolean; strict?: boolean }
26
+ ) {
27
+ console.log(pc.bold('\nAkropolys Inspect'));
28
+ console.log(pc.dim('─────────────────────────'));
29
+
30
+ let rawData = '';
31
+
32
+ // 1. Read input
33
+ try {
34
+ if (options.stdin || (!filePath && !process.stdin.isTTY)) {
35
+ rawData = await readStdin();
36
+ } else if (filePath) {
37
+ if (!fs.existsSync(filePath)) {
38
+ console.error(pc.red(`❌ Error: File not found at "${filePath}"`));
39
+ process.exit(1);
40
+ }
41
+ rawData = fs.readFileSync(filePath, 'utf-8');
42
+ } else {
43
+ console.error(pc.red('❌ Error: Provide a file path or pipe via standard input using --stdin'));
44
+ process.exit(1);
45
+ }
46
+ } catch (err: any) {
47
+ console.error(pc.red(`❌ Error reading input: ${err.message}`));
48
+ process.exit(1);
49
+ }
50
+
51
+ // 2. Parse JSON
52
+ let items: any[] = [];
53
+ try {
54
+ const parsed = JSON.parse(rawData.trim());
55
+ items = Array.isArray(parsed) ? parsed : [parsed];
56
+ } catch (err: any) {
57
+ console.error(pc.red(`❌ Error parsing JSON: Invalid JSON structure. ${err.message}`));
58
+ process.exit(1);
59
+ }
60
+
61
+ console.log(pc.cyan(`Parsed ${items.length} catalog items.`));
62
+ console.log(pc.dim('\nIngestion Quality Diagnostics:'));
63
+
64
+ let warningsCount = 0;
65
+
66
+ // 3. Scan items against Registry Rules
67
+ items.forEach((item, index) => {
68
+ const identifier = item.id || item.productId || item.slug || item.url || item.name || `item at index ${index}`;
69
+
70
+ // AP001: Missing Stable Identifier
71
+ const hasId = item.id !== undefined && item.id !== null && item.id !== '';
72
+ const hasProductId = item.productId !== undefined && item.productId !== null && item.productId !== '';
73
+ const hasSlug = item.slug !== undefined && item.slug !== null && item.slug !== '';
74
+ const hasUrl = item.url !== undefined && item.url !== null && item.url !== '';
75
+ const hasName = item.name !== undefined && item.name !== null && item.name !== '';
76
+
77
+ if (!hasId && !hasProductId && !hasSlug && !hasUrl && !hasName) {
78
+ console.log(` ${pc.yellow('⚠')} [AP001] Missing Stable Identifier: "${identifier}" ➔ Deduplication & correlation unavailable`);
79
+ warningsCount++;
80
+ }
81
+
82
+ // AP002: Low-Signal Payload (sparse payload: fewer than 2 keys, or only identifier is defined)
83
+ const keysCount = Object.keys(item).length;
84
+ if (keysCount < 2) {
85
+ console.log(` ${pc.yellow('⚠')} [AP002] Low-Signal Payload: "${identifier}" has sparse attributes ➔ Search vector quality reduced`);
86
+ warningsCount++;
87
+ }
88
+ });
89
+
90
+ console.log(pc.dim('\n─────────────────────────'));
91
+ console.log(pc.bold(`Inspect complete: ${items.length} items checked, ${warningsCount} warnings flagged.`));
92
+
93
+ // 4. Exit codes: strict vs non-strict
94
+ if (warningsCount > 0 && options.strict) {
95
+ console.log(pc.red('Exit code 3: strict mode failed due to warnings.'));
96
+ process.exit(3);
97
+ }
98
+
99
+ process.exit(0);
100
+ }
package/src/index.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { Command } from 'commander';
2
+ import { runInit } from './commands/init';
3
+ import { runDoctor } from './commands/doctor';
4
+ import { runInspect } from './commands/inspect';
5
+
6
+ const program = new Command();
7
+
8
+ program
9
+ .name('akropolys')
10
+ .description('Akropolys Command Line Tool — Developer diagnostics, setup helper, and structural inspector.')
11
+ .version('1.0.0');
12
+
13
+ program
14
+ .command('init')
15
+ .description('Configure the local workspace by generating a default .env file template.')
16
+ .action(async () => {
17
+ await runInit();
18
+ });
19
+
20
+ program
21
+ .command('doctor')
22
+ .description('Perform a health check consolidating local configuration values and backend API reachability.')
23
+ .option('-v, --verbose', 'Include verbose logs and configuration dumps.')
24
+ .action(async (options) => {
25
+ await runDoctor(options);
26
+ });
27
+
28
+ program
29
+ .command('inspect')
30
+ .description('Statically inspect a catalog payload file or stream against the Akropolys Anti-Pattern Registry.')
31
+ .argument('[file]', 'Path to the local catalog JSON file.')
32
+ .option('--stdin', 'Force reading data from standard input.')
33
+ .option('--strict', 'Fail the process (exit code 3) if any structural registry warnings are triggered.')
34
+ .action(async (file, options) => {
35
+ await runInspect(file, options);
36
+ });
37
+
38
+ program.parse(process.argv);
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "esModuleInterop": true,
7
+ "forceConsistentCasingInFileNames": true,
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "outDir": "./dist"
11
+ },
12
+ "include": ["src/**/*"]
13
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['cjs'],
6
+ clean: true,
7
+ dts: false,
8
+ banner: {
9
+ js: '#!/usr/bin/env node',
10
+ },
11
+ });