@sansavision/aurora 0.1.0-alpha.20260212.4
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/README.md +4 -0
- package/package.json +17 -0
- package/src/ai-diagnostics.ts +156 -0
- package/src/ai.ts +574 -0
- package/src/analyze.ts +669 -0
- package/src/bin/aurora.ts +15 -0
- package/src/build.ts +431 -0
- package/src/bun-test-shims.d.ts +17 -0
- package/src/create-feature.ts +419 -0
- package/src/create-route.ts +581 -0
- package/src/create.ts +425 -0
- package/src/dev.ts +126 -0
- package/src/devtools.ts +1143 -0
- package/src/doctor.ts +611 -0
- package/src/explain.ts +855 -0
- package/src/help.ts +39 -0
- package/src/index.ts +34 -0
- package/src/init.ts +1011 -0
- package/src/inspect-cache.ts +464 -0
- package/src/lsp-inline-hints.ts +254 -0
- package/src/node-shims.d.ts +26 -0
- package/src/process.d.ts +11 -0
- package/src/query-profiler.ts +520 -0
- package/src/realtime-monitor.ts +389 -0
- package/src/registry.ts +303 -0
- package/src/run.ts +37 -0
- package/src/start.ts +56 -0
- package/src/test.ts +289 -0
- package/templates/basic/README.md +16 -0
- package/templates/basic/package.json +10 -0
- package/templates/basic/src/actions/createMessage.action.server.ts +22 -0
- package/templates/basic/src/lib/auth.server.ts +11 -0
- package/templates/basic/src/queries/listMessages.server.ts +17 -0
- package/templates/basic/src/routes/index.tsx +12 -0
- package/templates/blog/README.md +17 -0
- package/templates/blog/package.json +12 -0
- package/templates/blog/public/assets/og-default.svg +17 -0
- package/templates/blog/src/content/loadPosts.server.ts +22 -0
- package/templates/blog/src/content/posts/hello-world.md +11 -0
- package/templates/blog/src/content/posts/release-notes.md +9 -0
- package/templates/blog/src/routes/index.tsx +22 -0
- package/templates/blog/src/routes/posts/[slug].tsx +19 -0
- package/templates/blog/src/seo/meta.ts +19 -0
- package/templates/dashboard/README.md +18 -0
- package/templates/dashboard/package.json +10 -0
- package/templates/dashboard/src/actions/acknowledgeAlert.action.server.ts +6 -0
- package/templates/dashboard/src/queries/getDashboardMetrics.server.ts +30 -0
- package/templates/dashboard/src/realtime/useDashboardRealtime.client.ts +13 -0
- package/templates/dashboard/src/routes/index.tsx +19 -0
- package/templates/dashboard/src/widgets/DataGrid.client.ts +8 -0
- package/templates/dashboard/src/widgets/MetricChart.client.ts +8 -0
- package/templates/desktop/README.md +18 -0
- package/templates/desktop/package.json +11 -0
- package/templates/desktop/src/actions/saveDesktopPreference.action.server.ts +28 -0
- package/templates/desktop/src/desktop/secureStorage.client.ts +20 -0
- package/templates/desktop/src/desktop/tauriBridge.client.ts +14 -0
- package/templates/desktop/src/queries/getDesktopSyncStatus.server.ts +9 -0
- package/templates/desktop/src/routes/index.tsx +27 -0
- package/templates/desktop/src/sync/offlineSyncBoundary.server.ts +27 -0
- package/templates/feature-skeleton/README.md +13 -0
- package/templates/feature-skeleton/actions/createFeature.action.server.ts +19 -0
- package/templates/feature-skeleton/index.ts +8 -0
- package/templates/feature-skeleton/queries/listFeature.server.ts +15 -0
- package/templates/feature-skeleton/realtime/useFeatureRealtime.client.ts +16 -0
- package/templates/feature-skeleton/template.manifest.json +15 -0
- package/templates/feature-skeleton/ui/FeatureView.client.tsx +14 -0
- package/templates/mobile/README.md +17 -0
- package/templates/mobile/package.json +11 -0
- package/templates/mobile/src/mobile/auth/session-handoff.client.ts +69 -0
- package/templates/mobile/src/mobile/generated/mobile-api-sdk.ts +62 -0
- package/templates/mobile/src/mobile/transport/mobile-api-transport.client.ts +122 -0
- package/templates/mobile/src/routes/index.tsx +134 -0
- package/templates/monorepo/README.md +18 -0
- package/templates/monorepo/apps/web/package.json +9 -0
- package/templates/monorepo/apps/web/src/routes/index.tsx +1 -0
- package/templates/monorepo/package.json +13 -0
- package/templates/monorepo/packages/shared/README.md +3 -0
- package/templates/monorepo/packages/ui/README.md +3 -0
- package/templates/saas/README.md +17 -0
- package/templates/saas/package.json +10 -0
- package/templates/saas/src/admin/getDashboard.server.ts +18 -0
- package/templates/saas/src/auth/session.server.ts +13 -0
- package/templates/saas/src/billing/checkout.server.ts +11 -0
- package/templates/saas/src/email/sendWelcome.server.ts +8 -0
- package/templates/saas/src/realtime/notifications.server.ts +8 -0
- package/templates/saas/src/routes/index.tsx +20 -0
- package/test/ai.test.ts +94 -0
- package/test/analyze.test.ts +301 -0
- package/test/build.test.ts +135 -0
- package/test/create-feature.test.ts +145 -0
- package/test/create-route.test.ts +117 -0
- package/test/create.test.ts +222 -0
- package/test/dev.test.ts +52 -0
- package/test/devtools.test.ts +130 -0
- package/test/doctor.test.ts +129 -0
- package/test/explain.test.ts +232 -0
- package/test/feature-skeleton.test.ts +53 -0
- package/test/fixtures/analyze/cache-input.invalid.json +1 -0
- package/test/fixtures/analyze/cache-input.missing-keyhash.v1.json +10 -0
- package/test/fixtures/analyze/cache-input.unsupported-version.v2.json +10 -0
- package/test/fixtures/analyze/cache-input.v1.json +12 -0
- package/test/fixtures/analyze/compiler-manifest/manifest.json +11 -0
- package/test/fixtures/analyze/guardrails-input.unsupported-version.v2.json +4 -0
- package/test/fixtures/analyze/guardrails-input.v1.json +49 -0
- package/test/fixtures/analyze/query-input.invalid-cache-status.v1.json +11 -0
- package/test/fixtures/analyze/query-input.unsupported-version.v2.json +11 -0
- package/test/fixtures/analyze/query-input.v1.json +18 -0
- package/test/fixtures/analyze/realtime-input.missing-lag-p95.v1.json +10 -0
- package/test/fixtures/analyze/realtime-input.unsupported-version.v2.json +8 -0
- package/test/fixtures/analyze/realtime-input.v1.json +12 -0
- package/test/fixtures/cache-inspector/cache-input.v1.json +23 -0
- package/test/fixtures/cache-inspector/invalid.json +1 -0
- package/test/fixtures/cache-inspector/snapshot.v1.json +34 -0
- package/test/fixtures/cache-inspector/unsupported-version.v2.json +13 -0
- package/test/fixtures/devtools/healthy.v1.json +130 -0
- package/test/fixtures/devtools/invalid.json +1 -0
- package/test/fixtures/devtools/unsupported-version.v2.json +8 -0
- package/test/fixtures/devtools/warn.v1.json +114 -0
- package/test/fixtures/doctor/clean/src/page.tsx +3 -0
- package/test/fixtures/doctor/findings/src/accessibility.client.tsx +7 -0
- package/test/fixtures/doctor/findings/src/migration.config.ts +3 -0
- package/test/fixtures/doctor/findings/src/page.client.tsx +5 -0
- package/test/fixtures/doctor/findings/src/perf.server.ts +15 -0
- package/test/fixtures/doctor/findings/src/routes.js +3 -0
- package/test/fixtures/doctor/findings/src/security.server.ts +7 -0
- package/test/fixtures/doctor/findings/src/users.server.ts +3 -0
- package/test/fixtures/doctor/governance/src/features/analytics/OWNERS.ts +2 -0
- package/test/fixtures/doctor/governance/src/features/analytics/page.tsx +3 -0
- package/test/fixtures/doctor/governance/src/features/billing/page.tsx +3 -0
- package/test/fixtures/explain/invalid.json +1 -0
- package/test/fixtures/explain/module-report.unsupported-version.v2.json +6 -0
- package/test/fixtures/explain/module-report.v1.json +72 -0
- package/test/fixtures/query-profiler/healthy.v1.json +11 -0
- package/test/fixtures/query-profiler/invalid.json +1 -0
- package/test/fixtures/query-profiler/unsupported-version.v2.json +6 -0
- package/test/fixtures/query-profiler/warning.v1.json +10 -0
- package/test/fixtures/realtime-monitor/healthy.v1.json +8 -0
- package/test/fixtures/realtime-monitor/invalid.json +1 -0
- package/test/fixtures/realtime-monitor/unsupported-version.v2.json +8 -0
- package/test/fixtures/realtime-monitor/warning.v1.json +8 -0
- package/test/help-parity.test.ts +104 -0
- package/test/init.test.ts +164 -0
- package/test/inspect-cache.test.ts +112 -0
- package/test/lsp-inline-hints.test.ts +65 -0
- package/test/query-profiler.test.ts +123 -0
- package/test/realtime-monitor.test.ts +115 -0
- package/test/registry.test.ts +41 -0
- package/test/start.test.ts +23 -0
- package/test/test-command.test.ts +65 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { type CommandContext, type CommandResult } from "./registry";
|
|
5
|
+
|
|
6
|
+
type InspectorFormat = "text" | "json";
|
|
7
|
+
|
|
8
|
+
interface InspectorOptions {
|
|
9
|
+
inputPath: string;
|
|
10
|
+
format: InspectorFormat;
|
|
11
|
+
limit: number;
|
|
12
|
+
tagFilter?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface InspectableEntry {
|
|
16
|
+
key: string;
|
|
17
|
+
tags: string[];
|
|
18
|
+
sizeBytes: number;
|
|
19
|
+
stale: boolean;
|
|
20
|
+
expiresAt?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface TagCount {
|
|
24
|
+
tag: string;
|
|
25
|
+
entries: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface CacheInspectorLargestEntry {
|
|
29
|
+
key: string;
|
|
30
|
+
tags: string[];
|
|
31
|
+
sizeBytes: number;
|
|
32
|
+
stale: boolean;
|
|
33
|
+
expiresAt?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface CacheInspectorReport {
|
|
37
|
+
mode: "cache-inspector";
|
|
38
|
+
projectRoot: string;
|
|
39
|
+
inputPath: string;
|
|
40
|
+
entries: number;
|
|
41
|
+
filteredEntries: number;
|
|
42
|
+
staleEntries: number;
|
|
43
|
+
totalSizeBytes: number;
|
|
44
|
+
uniqueTags: number;
|
|
45
|
+
topTags: TagCount[];
|
|
46
|
+
largestEntries: CacheInspectorLargestEntry[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const MAX_LIMIT = 500;
|
|
50
|
+
const DEFAULT_LIMIT = 20;
|
|
51
|
+
|
|
52
|
+
function parsePositiveLimit(raw: string | undefined): number | CommandResult {
|
|
53
|
+
if (!raw) {
|
|
54
|
+
return {
|
|
55
|
+
exitCode: 2,
|
|
56
|
+
stderr: "aurora inspect-cache: --limit requires an integer value",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const parsed = Number(raw);
|
|
61
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > MAX_LIMIT) {
|
|
62
|
+
return {
|
|
63
|
+
exitCode: 2,
|
|
64
|
+
stderr: `aurora inspect-cache: --limit must be an integer between 1 and ${MAX_LIMIT}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return parsed;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseInspectorOptions(args: ReadonlyArray<string>): InspectorOptions | CommandResult {
|
|
72
|
+
const options: InspectorOptions = {
|
|
73
|
+
inputPath: "",
|
|
74
|
+
format: "text",
|
|
75
|
+
limit: DEFAULT_LIMIT,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
79
|
+
const arg = args[i];
|
|
80
|
+
if (arg === "--input") {
|
|
81
|
+
const value = args[i + 1];
|
|
82
|
+
if (!value) {
|
|
83
|
+
return {
|
|
84
|
+
exitCode: 2,
|
|
85
|
+
stderr: "aurora inspect-cache: --input requires a path value",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
options.inputPath = value;
|
|
90
|
+
i += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (arg === "--format") {
|
|
95
|
+
const value = args[i + 1];
|
|
96
|
+
if (!value) {
|
|
97
|
+
return {
|
|
98
|
+
exitCode: 2,
|
|
99
|
+
stderr: "aurora inspect-cache: --format requires 'text' or 'json'",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (value !== "text" && value !== "json") {
|
|
104
|
+
return {
|
|
105
|
+
exitCode: 2,
|
|
106
|
+
stderr: `aurora inspect-cache: invalid format '${value}'. Expected 'text' or 'json'`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
options.format = value;
|
|
111
|
+
i += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (arg === "--tag") {
|
|
116
|
+
const value = args[i + 1];
|
|
117
|
+
if (!value || value.trim().length === 0) {
|
|
118
|
+
return {
|
|
119
|
+
exitCode: 2,
|
|
120
|
+
stderr: "aurora inspect-cache: --tag requires a non-empty tag value",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
options.tagFilter = value.trim();
|
|
125
|
+
i += 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (arg === "--limit") {
|
|
130
|
+
const parsed = parsePositiveLimit(args[i + 1]);
|
|
131
|
+
if (typeof parsed !== "number") {
|
|
132
|
+
return parsed;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
options.limit = parsed;
|
|
136
|
+
i += 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
exitCode: 2,
|
|
142
|
+
stderr: `aurora inspect-cache: unknown option '${arg}'`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!options.inputPath) {
|
|
147
|
+
return {
|
|
148
|
+
exitCode: 2,
|
|
149
|
+
stderr: "aurora inspect-cache: --input is required",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return options;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function toRecord(value: unknown): Record<string, unknown> | undefined {
|
|
157
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return value as Record<string, unknown>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isCommandResult(value: unknown): value is CommandResult {
|
|
165
|
+
const source = toRecord(value);
|
|
166
|
+
return Boolean(source && typeof source.exitCode === "number");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function getOptionalNumber(
|
|
170
|
+
source: Record<string, unknown>,
|
|
171
|
+
fields: ReadonlyArray<string>,
|
|
172
|
+
): number | undefined {
|
|
173
|
+
for (const field of fields) {
|
|
174
|
+
const value = source[field];
|
|
175
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getOptionalBoolean(
|
|
184
|
+
source: Record<string, unknown>,
|
|
185
|
+
fields: ReadonlyArray<string>,
|
|
186
|
+
): boolean | undefined {
|
|
187
|
+
for (const field of fields) {
|
|
188
|
+
const value = source[field];
|
|
189
|
+
if (typeof value === "boolean") {
|
|
190
|
+
return value;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeTags(raw: unknown): string[] {
|
|
198
|
+
const source =
|
|
199
|
+
typeof raw === "string"
|
|
200
|
+
? [raw]
|
|
201
|
+
: Array.isArray(raw)
|
|
202
|
+
? raw.filter((item): item is string => typeof item === "string")
|
|
203
|
+
: [];
|
|
204
|
+
|
|
205
|
+
const unique = new Set(
|
|
206
|
+
source
|
|
207
|
+
.map((tag) => tag.trim())
|
|
208
|
+
.filter((tag) => tag.length > 0),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
return [...unique].sort();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function getRequiredKey(source: Record<string, unknown>, entryIndex: number): string {
|
|
215
|
+
const fields = ["key", "hash", "cacheKey"];
|
|
216
|
+
for (const field of fields) {
|
|
217
|
+
const value = source[field];
|
|
218
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
219
|
+
return value.trim();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
throw new Error(`entry ${entryIndex}: missing key field (key/hash/cacheKey)`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getEntrySizeBytes(source: Record<string, unknown>): number {
|
|
227
|
+
const rawSize = source.sizeBytes;
|
|
228
|
+
if (typeof rawSize === "number" && Number.isFinite(rawSize) && rawSize >= 0) {
|
|
229
|
+
return Math.trunc(rawSize);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const serialized = JSON.stringify(source.value);
|
|
234
|
+
if (typeof serialized === "string") {
|
|
235
|
+
return serialized.length;
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
// Fall through to default.
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function readJsonPayload(inputPath: string, context: CommandContext): unknown | CommandResult {
|
|
245
|
+
const absolutePath = resolve(context.cwd, inputPath);
|
|
246
|
+
let raw = "";
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
raw = readFileSync(absolutePath, "utf8");
|
|
250
|
+
} catch {
|
|
251
|
+
return {
|
|
252
|
+
exitCode: 1,
|
|
253
|
+
stderr: `aurora inspect-cache: unable to read input at ${inputPath}`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
return JSON.parse(raw);
|
|
259
|
+
} catch {
|
|
260
|
+
return {
|
|
261
|
+
exitCode: 1,
|
|
262
|
+
stderr: `aurora inspect-cache: invalid json input at ${inputPath}`,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function assertSupportedInputVersion(payload: unknown): void {
|
|
268
|
+
const source = toRecord(payload);
|
|
269
|
+
if (!source) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const version = source.version;
|
|
274
|
+
if (version !== undefined && version !== 1) {
|
|
275
|
+
throw new Error(`unsupported cache snapshot version ${String(version)}; expected 1`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const schemaVersion = source.schemaVersion;
|
|
279
|
+
if (schemaVersion !== undefined && schemaVersion !== 1) {
|
|
280
|
+
throw new Error(`unsupported schemaVersion ${String(schemaVersion)}; expected 1`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function normalizePayloadEntries(payload: unknown): unknown[] {
|
|
285
|
+
if (Array.isArray(payload)) {
|
|
286
|
+
return payload;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const source = toRecord(payload);
|
|
290
|
+
if (source && Array.isArray(source.entries)) {
|
|
291
|
+
return source.entries;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
throw new Error("cache inspector input must be an array or an object with entries[]");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function normalizeEntry(raw: unknown, entryIndex: number, now: number): InspectableEntry {
|
|
298
|
+
const source = toRecord(raw);
|
|
299
|
+
if (!source) {
|
|
300
|
+
throw new Error(`entry ${entryIndex}: expected an object`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const key = getRequiredKey(source, entryIndex);
|
|
304
|
+
const tags = normalizeTags(source.tags ?? source.labels ?? source.tag);
|
|
305
|
+
const sizeBytes = getEntrySizeBytes(source);
|
|
306
|
+
const expiresAt = getOptionalNumber(source, ["expiresAt", "expires_at"]);
|
|
307
|
+
const staleExplicit = getOptionalBoolean(source, ["stale", "isStale"]);
|
|
308
|
+
const stale = staleExplicit ?? (expiresAt !== undefined && expiresAt <= now);
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
key,
|
|
312
|
+
tags,
|
|
313
|
+
sizeBytes,
|
|
314
|
+
stale,
|
|
315
|
+
expiresAt,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function compareLargestEntries(left: InspectableEntry, right: InspectableEntry): number {
|
|
320
|
+
if (right.sizeBytes !== left.sizeBytes) {
|
|
321
|
+
return right.sizeBytes - left.sizeBytes;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return left.key.localeCompare(right.key);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function buildReport(
|
|
328
|
+
options: InspectorOptions,
|
|
329
|
+
context: CommandContext,
|
|
330
|
+
): CacheInspectorReport | CommandResult {
|
|
331
|
+
const payload = readJsonPayload(options.inputPath, context);
|
|
332
|
+
if (isCommandResult(payload)) {
|
|
333
|
+
return payload;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let normalized: InspectableEntry[];
|
|
337
|
+
try {
|
|
338
|
+
assertSupportedInputVersion(payload);
|
|
339
|
+
const entries = normalizePayloadEntries(payload);
|
|
340
|
+
const now = Date.now();
|
|
341
|
+
normalized = entries.map((entry, index) => normalizeEntry(entry, index, now));
|
|
342
|
+
} catch (error) {
|
|
343
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
344
|
+
return {
|
|
345
|
+
exitCode: 1,
|
|
346
|
+
stderr: `aurora inspect-cache: ${reason}`,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const tagFilter = options.tagFilter;
|
|
351
|
+
const filtered =
|
|
352
|
+
tagFilter !== undefined
|
|
353
|
+
? normalized.filter((entry) => entry.tags.includes(tagFilter))
|
|
354
|
+
: normalized;
|
|
355
|
+
|
|
356
|
+
const tagCounts = new Map<string, number>();
|
|
357
|
+
for (const entry of filtered) {
|
|
358
|
+
for (const tag of entry.tags) {
|
|
359
|
+
tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const topTags: TagCount[] = [...tagCounts.entries()]
|
|
364
|
+
.map(([tag, entries]) => ({ tag, entries }))
|
|
365
|
+
.sort((left, right) => {
|
|
366
|
+
if (right.entries !== left.entries) {
|
|
367
|
+
return right.entries - left.entries;
|
|
368
|
+
}
|
|
369
|
+
return left.tag.localeCompare(right.tag);
|
|
370
|
+
})
|
|
371
|
+
.slice(0, 10);
|
|
372
|
+
|
|
373
|
+
const largestEntries = [...filtered]
|
|
374
|
+
.sort(compareLargestEntries)
|
|
375
|
+
.slice(0, options.limit)
|
|
376
|
+
.map((entry) => ({
|
|
377
|
+
key: entry.key,
|
|
378
|
+
tags: entry.tags,
|
|
379
|
+
sizeBytes: entry.sizeBytes,
|
|
380
|
+
stale: entry.stale,
|
|
381
|
+
expiresAt: entry.expiresAt,
|
|
382
|
+
}));
|
|
383
|
+
|
|
384
|
+
const totalSizeBytes = filtered.reduce((sum, entry) => sum + entry.sizeBytes, 0);
|
|
385
|
+
const staleEntries = filtered.filter((entry) => entry.stale).length;
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
mode: "cache-inspector",
|
|
389
|
+
projectRoot: context.cwd,
|
|
390
|
+
inputPath: options.inputPath,
|
|
391
|
+
entries: normalized.length,
|
|
392
|
+
filteredEntries: filtered.length,
|
|
393
|
+
staleEntries,
|
|
394
|
+
totalSizeBytes,
|
|
395
|
+
uniqueTags: tagCounts.size,
|
|
396
|
+
topTags,
|
|
397
|
+
largestEntries,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function renderTextReport(report: CacheInspectorReport, options: InspectorOptions): string {
|
|
402
|
+
const lines = [
|
|
403
|
+
"aurora cache inspector report",
|
|
404
|
+
`project_root: ${report.projectRoot}`,
|
|
405
|
+
`input_path: ${report.inputPath}`,
|
|
406
|
+
`entries_total: ${report.entries}`,
|
|
407
|
+
`entries_filtered: ${report.filteredEntries}`,
|
|
408
|
+
`stale_entries: ${report.staleEntries}`,
|
|
409
|
+
`total_size_bytes: ${report.totalSizeBytes}`,
|
|
410
|
+
`unique_tags: ${report.uniqueTags}`,
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
if (options.tagFilter) {
|
|
414
|
+
lines.push(`filter_tag: ${options.tagFilter}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (report.topTags.length > 0) {
|
|
418
|
+
lines.push("top_tags:");
|
|
419
|
+
for (const tag of report.topTags) {
|
|
420
|
+
lines.push(`- ${tag.tag}: ${tag.entries}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (report.largestEntries.length > 0) {
|
|
425
|
+
lines.push("largest_entries:");
|
|
426
|
+
for (const entry of report.largestEntries) {
|
|
427
|
+
const tags = entry.tags.length > 0 ? entry.tags.join(",") : "-";
|
|
428
|
+
const expiresAt =
|
|
429
|
+
entry.expiresAt !== undefined ? ` expires_at=${entry.expiresAt}` : "";
|
|
430
|
+
lines.push(
|
|
431
|
+
`- ${entry.key} size_bytes=${entry.sizeBytes} stale=${entry.stale} tags=${tags}${expiresAt}`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return lines.join("\n");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function runInspectCacheCommand(
|
|
440
|
+
args: ReadonlyArray<string>,
|
|
441
|
+
context: CommandContext,
|
|
442
|
+
): CommandResult {
|
|
443
|
+
const options = parseInspectorOptions(args);
|
|
444
|
+
if ("exitCode" in options) {
|
|
445
|
+
return options;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const report = buildReport(options, context);
|
|
449
|
+
if ("exitCode" in report) {
|
|
450
|
+
return report;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (options.format === "json") {
|
|
454
|
+
return {
|
|
455
|
+
exitCode: 0,
|
|
456
|
+
stdout: JSON.stringify(report, null, 2),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
exitCode: 0,
|
|
462
|
+
stdout: renderTextReport(report, options),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExplainModuleReportV1,
|
|
3
|
+
ExplainQueryEntry,
|
|
4
|
+
ExplainSource,
|
|
5
|
+
JsonValue,
|
|
6
|
+
} from "./explain";
|
|
7
|
+
|
|
8
|
+
export type AuroraInlineHintKind =
|
|
9
|
+
| "key"
|
|
10
|
+
| "tags"
|
|
11
|
+
| "invalidation"
|
|
12
|
+
| "auth"
|
|
13
|
+
| "runtime";
|
|
14
|
+
|
|
15
|
+
export interface AuroraInlineHint {
|
|
16
|
+
kind: AuroraInlineHintKind;
|
|
17
|
+
symbol: string;
|
|
18
|
+
label: string;
|
|
19
|
+
source: ExplainSource;
|
|
20
|
+
position: {
|
|
21
|
+
line: number;
|
|
22
|
+
character: number;
|
|
23
|
+
};
|
|
24
|
+
paddingLeft: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createInlineHintsFromExplainReport(
|
|
28
|
+
sourceText: string,
|
|
29
|
+
report: Pick<ExplainModuleReportV1, "queries" | "actions">,
|
|
30
|
+
): readonly AuroraInlineHint[] {
|
|
31
|
+
const lines = sourceText.split(/\r?\n/);
|
|
32
|
+
const hints: AuroraInlineHint[] = [];
|
|
33
|
+
|
|
34
|
+
for (const query of report.queries) {
|
|
35
|
+
const line = findSymbolLine(lines, query.name);
|
|
36
|
+
if (line === undefined) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const character = lines[line]?.length ?? 0;
|
|
41
|
+
hints.push(buildQueryHint("key", query, line, character));
|
|
42
|
+
hints.push(buildQueryHint("tags", query, line, character));
|
|
43
|
+
|
|
44
|
+
if (query.auth) {
|
|
45
|
+
hints.push({
|
|
46
|
+
kind: "auth",
|
|
47
|
+
symbol: query.name,
|
|
48
|
+
label: `auth: ${query.auth.value}`,
|
|
49
|
+
source: query.auth.source,
|
|
50
|
+
position: { line, character },
|
|
51
|
+
paddingLeft: true,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const action of report.actions) {
|
|
57
|
+
const line = findSymbolLine(lines, action.name);
|
|
58
|
+
if (line === undefined) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const character = lines[line]?.length ?? 0;
|
|
63
|
+
hints.push({
|
|
64
|
+
kind: "invalidation",
|
|
65
|
+
symbol: action.name,
|
|
66
|
+
label: `invalidates: ${formatStringList(action.invalidates.value)}`,
|
|
67
|
+
source: action.invalidates.source,
|
|
68
|
+
position: { line, character },
|
|
69
|
+
paddingLeft: true,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (action.auth) {
|
|
73
|
+
hints.push({
|
|
74
|
+
kind: "auth",
|
|
75
|
+
symbol: action.name,
|
|
76
|
+
label: `auth: ${action.auth.value}`,
|
|
77
|
+
source: action.auth.source,
|
|
78
|
+
position: { line, character },
|
|
79
|
+
paddingLeft: true,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const runtimeExport = findMetadataExport(lines, "runtime");
|
|
85
|
+
if (runtimeExport) {
|
|
86
|
+
hints.push({
|
|
87
|
+
kind: "runtime",
|
|
88
|
+
symbol: "module",
|
|
89
|
+
label: `runtime: ${runtimeExport.value}`,
|
|
90
|
+
source: "explicit",
|
|
91
|
+
position: {
|
|
92
|
+
line: runtimeExport.line,
|
|
93
|
+
character: lines[runtimeExport.line]?.length ?? 0,
|
|
94
|
+
},
|
|
95
|
+
paddingLeft: true,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const authExport = findMetadataExport(lines, "auth");
|
|
100
|
+
if (authExport) {
|
|
101
|
+
hints.push({
|
|
102
|
+
kind: "auth",
|
|
103
|
+
symbol: "module",
|
|
104
|
+
label: `auth: ${authExport.value}`,
|
|
105
|
+
source: "explicit",
|
|
106
|
+
position: {
|
|
107
|
+
line: authExport.line,
|
|
108
|
+
character: lines[authExport.line]?.length ?? 0,
|
|
109
|
+
},
|
|
110
|
+
paddingLeft: true,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
hints.sort((left, right) => {
|
|
115
|
+
const byLine = left.position.line - right.position.line;
|
|
116
|
+
if (byLine !== 0) {
|
|
117
|
+
return byLine;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const byCharacter = left.position.character - right.position.character;
|
|
121
|
+
if (byCharacter !== 0) {
|
|
122
|
+
return byCharacter;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const bySymbol = left.symbol.localeCompare(right.symbol);
|
|
126
|
+
if (bySymbol !== 0) {
|
|
127
|
+
return bySymbol;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const byKind = left.kind.localeCompare(right.kind);
|
|
131
|
+
if (byKind !== 0) {
|
|
132
|
+
return byKind;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return left.label.localeCompare(right.label);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return dedupeHints(hints);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function buildQueryHint(
|
|
142
|
+
kind: "key" | "tags",
|
|
143
|
+
query: ExplainQueryEntry,
|
|
144
|
+
line: number,
|
|
145
|
+
character: number,
|
|
146
|
+
): AuroraInlineHint {
|
|
147
|
+
if (kind === "key") {
|
|
148
|
+
return {
|
|
149
|
+
kind,
|
|
150
|
+
symbol: query.name,
|
|
151
|
+
label: `key: ${formatKey(query.key.value)}`,
|
|
152
|
+
source: query.key.source,
|
|
153
|
+
position: { line, character },
|
|
154
|
+
paddingLeft: true,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
kind,
|
|
160
|
+
symbol: query.name,
|
|
161
|
+
label: `tags: ${formatStringList(query.tags.value)}`,
|
|
162
|
+
source: query.tags.source,
|
|
163
|
+
position: { line, character },
|
|
164
|
+
paddingLeft: true,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function formatKey(value: readonly JsonValue[] | string): string {
|
|
169
|
+
if (typeof value === "string") {
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return JSON.stringify(value);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function formatStringList(values: readonly string[]): string {
|
|
177
|
+
return values.length === 0 ? "[]" : `[${values.join(", ")}]`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function findSymbolLine(lines: readonly string[], symbol: string): number | undefined {
|
|
181
|
+
const escaped = escapeRegExp(symbol);
|
|
182
|
+
const exportConstPattern = new RegExp(`\\bexport\\s+const\\s+${escaped}\\b`);
|
|
183
|
+
const exportFunctionPattern = new RegExp(`\\bexport\\s+function\\s+${escaped}\\b`);
|
|
184
|
+
|
|
185
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
186
|
+
const line = lines[lineIndex] ?? "";
|
|
187
|
+
if (exportConstPattern.test(line) || exportFunctionPattern.test(line)) {
|
|
188
|
+
return lineIndex;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const symbolPattern = new RegExp(`\\b${escaped}\\b`);
|
|
193
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
194
|
+
const line = lines[lineIndex] ?? "";
|
|
195
|
+
if (symbolPattern.test(line)) {
|
|
196
|
+
return lineIndex;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function findMetadataExport(
|
|
204
|
+
lines: readonly string[],
|
|
205
|
+
exportName: "runtime" | "auth",
|
|
206
|
+
): { line: number; value: string } | undefined {
|
|
207
|
+
const pattern = new RegExp(
|
|
208
|
+
`^\\s*export\\s+const\\s+${exportName}\\s*=\\s*[\"']([^\"']+)[\"']`,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
212
|
+
const line = lines[lineIndex] ?? "";
|
|
213
|
+
const match = pattern.exec(line);
|
|
214
|
+
if (!match?.[1]) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
line: lineIndex,
|
|
220
|
+
value: match[1],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function escapeRegExp(value: string): string {
|
|
228
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function dedupeHints(hints: readonly AuroraInlineHint[]): readonly AuroraInlineHint[] {
|
|
232
|
+
const seen = new Set<string>();
|
|
233
|
+
const result: AuroraInlineHint[] = [];
|
|
234
|
+
|
|
235
|
+
for (const hint of hints) {
|
|
236
|
+
const key = [
|
|
237
|
+
hint.kind,
|
|
238
|
+
hint.symbol,
|
|
239
|
+
hint.label,
|
|
240
|
+
String(hint.source),
|
|
241
|
+
String(hint.position.line),
|
|
242
|
+
String(hint.position.character),
|
|
243
|
+
].join("|");
|
|
244
|
+
|
|
245
|
+
if (seen.has(key)) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
seen.add(key);
|
|
250
|
+
result.push(hint);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return result;
|
|
254
|
+
}
|