@f5xc-salesdemos/xcsh 19.30.0 → 19.30.1
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/package.json +8 -7
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/resource-management/index.ts +33 -14
- package/src/slash-commands/resource-commands.ts +26 -23
- package/src/resource-management/arg-parser.ts +0 -73
- package/src/resource-management/diff-engine.ts +0 -160
- package/src/resource-management/file-reader.ts +0 -163
- package/src/resource-management/kind-resolver.ts +0 -146
- package/src/resource-management/manifest-parser.ts +0 -72
- package/src/resource-management/manifest-validator.ts +0 -119
- package/src/resource-management/output-formatter.ts +0 -209
- package/src/resource-management/resource-client.ts +0 -370
- package/src/resource-management/types.ts +0 -103
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "19.30.
|
|
4
|
+
"version": "19.30.1",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5xc-salesdemos/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -50,12 +50,13 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
52
52
|
"@mozilla/readability": "^0.6",
|
|
53
|
-
"@f5xc-salesdemos/xcsh-stats": "19.30.
|
|
54
|
-
"@f5xc-salesdemos/pi-agent-core": "19.30.
|
|
55
|
-
"@f5xc-salesdemos/pi-ai": "19.30.
|
|
56
|
-
"@f5xc-salesdemos/pi-natives": "19.30.
|
|
57
|
-
"@f5xc-salesdemos/pi-
|
|
58
|
-
"@f5xc-salesdemos/pi-
|
|
53
|
+
"@f5xc-salesdemos/xcsh-stats": "19.30.1",
|
|
54
|
+
"@f5xc-salesdemos/pi-agent-core": "19.30.1",
|
|
55
|
+
"@f5xc-salesdemos/pi-ai": "19.30.1",
|
|
56
|
+
"@f5xc-salesdemos/pi-natives": "19.30.1",
|
|
57
|
+
"@f5xc-salesdemos/pi-resource-management": "19.30.1",
|
|
58
|
+
"@f5xc-salesdemos/pi-tui": "19.30.1",
|
|
59
|
+
"@f5xc-salesdemos/pi-utils": "19.30.1",
|
|
59
60
|
"@sinclair/typebox": "^0.34",
|
|
60
61
|
"@xterm/headless": "^6.0",
|
|
61
62
|
"ajv": "^8.20",
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "19.30.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "19.30.1",
|
|
21
|
+
"commit": "b1150372a182f6653cf5a62a53edb7d1c04c277a",
|
|
22
|
+
"shortCommit": "b115037",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.30.
|
|
25
|
-
"commitDate": "2026-06-
|
|
26
|
-
"buildDate": "2026-06-
|
|
24
|
+
"tag": "v19.30.1",
|
|
25
|
+
"commitDate": "2026-06-14T23:46:38Z",
|
|
26
|
+
"buildDate": "2026-06-15T00:08:14.358Z",
|
|
27
27
|
"dirty": true,
|
|
28
28
|
"prNumber": "",
|
|
29
29
|
"repoUrl": "https://github.com/f5xc-salesdemos/xcsh",
|
|
30
30
|
"repoSlug": "f5xc-salesdemos/xcsh",
|
|
31
|
-
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/
|
|
32
|
-
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.30.
|
|
31
|
+
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/b1150372a182f6653cf5a62a53edb7d1c04c277a",
|
|
32
|
+
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.30.1"
|
|
33
33
|
};
|
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
export { formatValidationErrors, validateManifest, validateManifests } from "./manifest-validator";
|
|
7
|
-
export {
|
|
8
|
-
formatMultiOperationSummary,
|
|
9
|
-
formatOperationResult,
|
|
10
|
-
formatResourceDetail,
|
|
11
|
-
formatResourceList,
|
|
12
|
-
} from "./output-formatter";
|
|
13
|
-
export { ResourceClient } from "./resource-client";
|
|
1
|
+
import { createKindResolver } from "@f5xc-salesdemos/pi-resource-management";
|
|
2
|
+
import { API_SPEC_INDEX, API_VALIDATION_DATA } from "../internal-urls/api-spec-index.generated";
|
|
3
|
+
|
|
4
|
+
export const kindResolver = createKindResolver(API_SPEC_INDEX, API_VALIDATION_DATA);
|
|
5
|
+
|
|
14
6
|
export type {
|
|
7
|
+
ApiSpecDomainEntry,
|
|
8
|
+
ApiSpecDomainResource,
|
|
9
|
+
ApiSpecIndex,
|
|
10
|
+
ApiSpecValidationResourceEntry,
|
|
11
|
+
DiffEntry,
|
|
12
|
+
KindResolver,
|
|
15
13
|
ManifestValidationResult,
|
|
16
14
|
OperationResult,
|
|
17
15
|
ParsedResourceArgs,
|
|
@@ -20,4 +18,25 @@ export type {
|
|
|
20
18
|
ResourceDiff,
|
|
21
19
|
ResourceError,
|
|
22
20
|
ResourceManifest,
|
|
23
|
-
|
|
21
|
+
ValidationError,
|
|
22
|
+
ValidationWarning,
|
|
23
|
+
} from "@f5xc-salesdemos/pi-resource-management";
|
|
24
|
+
export {
|
|
25
|
+
computeResourceDiff,
|
|
26
|
+
createKindResolver,
|
|
27
|
+
formatDiff,
|
|
28
|
+
formatMultiOperationSummary,
|
|
29
|
+
formatOperationResult,
|
|
30
|
+
formatResourceDetail,
|
|
31
|
+
formatResourceList,
|
|
32
|
+
formatValidationErrors,
|
|
33
|
+
KindResolutionError,
|
|
34
|
+
ManifestFileError,
|
|
35
|
+
ManifestParseError,
|
|
36
|
+
parseManifests,
|
|
37
|
+
parseResourceArgs,
|
|
38
|
+
ResourceClient,
|
|
39
|
+
readManifestFiles,
|
|
40
|
+
validateManifest,
|
|
41
|
+
validateManifests,
|
|
42
|
+
} from "@f5xc-salesdemos/pi-resource-management";
|
|
@@ -9,9 +9,8 @@ interface ParsedBuiltinSlashCommand {
|
|
|
9
9
|
|
|
10
10
|
function getKindCompletions(prefix: string): AutocompleteItem[] | null {
|
|
11
11
|
try {
|
|
12
|
-
const {
|
|
13
|
-
|
|
14
|
-
const kinds = getKindsWithApiPaths();
|
|
12
|
+
const { kindResolver } = require("../resource-management/index") as typeof import("../resource-management/index");
|
|
13
|
+
const kinds = kindResolver.getKindsWithApiPaths();
|
|
15
14
|
const lower = prefix.toLowerCase();
|
|
16
15
|
const items = kinds
|
|
17
16
|
.filter(k => k.toLowerCase().startsWith(lower))
|
|
@@ -31,9 +30,25 @@ export async function handleResourceCommand(
|
|
|
31
30
|
ctx.editor.addToHistory(command.text);
|
|
32
31
|
ctx.editor.setText("");
|
|
33
32
|
|
|
34
|
-
const {
|
|
35
|
-
|
|
33
|
+
const {
|
|
34
|
+
parseResourceArgs,
|
|
35
|
+
ResourceClient,
|
|
36
|
+
readManifestFiles,
|
|
37
|
+
ManifestFileError,
|
|
38
|
+
parseManifests,
|
|
39
|
+
ManifestParseError,
|
|
40
|
+
KindResolutionError,
|
|
41
|
+
validateManifest,
|
|
42
|
+
formatValidationErrors,
|
|
43
|
+
formatOperationResult,
|
|
44
|
+
formatResourceList,
|
|
45
|
+
formatResourceDetail,
|
|
46
|
+
formatMultiOperationSummary,
|
|
47
|
+
formatDiff,
|
|
48
|
+
} = await import("@f5xc-salesdemos/pi-resource-management");
|
|
49
|
+
const { kindResolver } = await import("../resource-management/index");
|
|
36
50
|
|
|
51
|
+
const parsed = parseResourceArgs(command.args);
|
|
37
52
|
if ("error" in parsed) {
|
|
38
53
|
ctx.showStatus(parsed.error);
|
|
39
54
|
return;
|
|
@@ -67,15 +82,6 @@ export async function handleResourceCommand(
|
|
|
67
82
|
return;
|
|
68
83
|
}
|
|
69
84
|
|
|
70
|
-
const { ResourceClient } = await import("../resource-management/resource-client");
|
|
71
|
-
const { readManifestFiles, ManifestFileError } = await import("../resource-management/file-reader");
|
|
72
|
-
const { parseManifests, ManifestParseError } = await import("../resource-management/manifest-parser");
|
|
73
|
-
const { resolveKind, KindResolutionError } = await import("../resource-management/kind-resolver");
|
|
74
|
-
const { validateManifest, formatValidationErrors } = await import("../resource-management/manifest-validator");
|
|
75
|
-
const { formatOperationResult, formatResourceList, formatResourceDetail, formatMultiOperationSummary } =
|
|
76
|
-
await import("../resource-management/output-formatter");
|
|
77
|
-
const { formatDiff } = await import("../resource-management/diff-engine");
|
|
78
|
-
|
|
79
85
|
const client = new ResourceClient({
|
|
80
86
|
apiUrl,
|
|
81
87
|
apiToken,
|
|
@@ -104,7 +110,7 @@ export async function handleResourceCommand(
|
|
|
104
110
|
const manifests = parseManifests(allObjects, fileResults[0]?.sourcePath ?? "input");
|
|
105
111
|
const results = [];
|
|
106
112
|
for (const manifest of manifests) {
|
|
107
|
-
const { result: valResult, resolved } = validateManifest(manifest, ns);
|
|
113
|
+
const { result: valResult, resolved } = validateManifest(manifest, kindResolver, ns);
|
|
108
114
|
if (!valResult.valid) {
|
|
109
115
|
ctx.showStatus(formatValidationErrors(manifest, valResult));
|
|
110
116
|
results.push({
|
|
@@ -116,10 +122,7 @@ export async function handleResourceCommand(
|
|
|
116
122
|
if (!resolved) continue;
|
|
117
123
|
|
|
118
124
|
if (parsed.dryRun === "client") {
|
|
119
|
-
results.push({
|
|
120
|
-
status: "dry-run" as const,
|
|
121
|
-
action: "create" as const,
|
|
122
|
-
});
|
|
125
|
+
results.push({ status: "dry-run" as const, action: "create" as const });
|
|
123
126
|
ctx.showStatus(
|
|
124
127
|
formatOperationResult({ status: "dry-run", action: "create" }, manifest, parsed.outputFormat),
|
|
125
128
|
);
|
|
@@ -157,7 +160,7 @@ export async function handleResourceCommand(
|
|
|
157
160
|
}
|
|
158
161
|
|
|
159
162
|
for (const target of deleteTargets) {
|
|
160
|
-
const resolved = resolveKind(target.kind);
|
|
163
|
+
const resolved = kindResolver.resolveKind(target.kind);
|
|
161
164
|
const result = await client.delete(target.kind, target.name, resolved, ns);
|
|
162
165
|
ctx.showStatus(
|
|
163
166
|
formatOperationResult(
|
|
@@ -181,7 +184,7 @@ export async function handleResourceCommand(
|
|
|
181
184
|
ctx.showStatus("Usage: /describe <kind> <name> [-n namespace] [-o json|yaml]");
|
|
182
185
|
return;
|
|
183
186
|
}
|
|
184
|
-
const resolved = resolveKind(kind);
|
|
187
|
+
const resolved = kindResolver.resolveKind(kind);
|
|
185
188
|
const result = await client.get(resolved, name, ns);
|
|
186
189
|
if (result.error) {
|
|
187
190
|
ctx.showStatus(`Error: ${result.error.message}`);
|
|
@@ -203,7 +206,7 @@ export async function handleResourceCommand(
|
|
|
203
206
|
const manifests = parseManifests(allObjects, fileResults[0]?.sourcePath ?? "input");
|
|
204
207
|
|
|
205
208
|
for (const manifest of manifests) {
|
|
206
|
-
const { resolved } = validateManifest(manifest, ns);
|
|
209
|
+
const { resolved } = validateManifest(manifest, kindResolver, ns);
|
|
207
210
|
if (!resolved) {
|
|
208
211
|
ctx.showStatus(`Unknown kind: "${manifest.kind}"`);
|
|
209
212
|
continue;
|
|
@@ -229,7 +232,7 @@ export async function handleResourceCommand(
|
|
|
229
232
|
ctx.showStatus("Usage: /get <kind> [name] [-n namespace] [-o json|yaml|table]");
|
|
230
233
|
return;
|
|
231
234
|
}
|
|
232
|
-
const resolved = resolveKind(parsed.kind);
|
|
235
|
+
const resolved = kindResolver.resolveKind(parsed.kind);
|
|
233
236
|
const result = await client.get(resolved, parsed.name, ns);
|
|
234
237
|
if (result.error) {
|
|
235
238
|
ctx.showStatus(`Error: ${result.error.message}`);
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import type { ParsedResourceArgs } from "./types";
|
|
2
|
-
|
|
3
|
-
const VALID_OUTPUT_FORMATS = new Set(["json", "yaml", "table", "wide"]);
|
|
4
|
-
const VALID_DRY_RUN_MODES = new Set(["client", "server"]);
|
|
5
|
-
|
|
6
|
-
export function parseResourceArgs(argsString: string): ParsedResourceArgs | { error: string } {
|
|
7
|
-
const tokens = argsString.split(/\s+/).filter(Boolean);
|
|
8
|
-
const filenames: string[] = [];
|
|
9
|
-
let namespace: string | undefined;
|
|
10
|
-
let outputFormat: ParsedResourceArgs["outputFormat"] = "table";
|
|
11
|
-
let dryRun: ParsedResourceArgs["dryRun"];
|
|
12
|
-
let recursive = false;
|
|
13
|
-
let force = false;
|
|
14
|
-
const positionals: string[] = [];
|
|
15
|
-
|
|
16
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
17
|
-
const token = tokens[i];
|
|
18
|
-
|
|
19
|
-
if (token === "-f" || token === "--filename") {
|
|
20
|
-
if (i + 1 >= tokens.length) return { error: `${token} requires a file path.` };
|
|
21
|
-
filenames.push(tokens[++i]);
|
|
22
|
-
} else if (token.startsWith("-f") && token.length > 2 && token[2] !== "-") {
|
|
23
|
-
filenames.push(token.slice(2));
|
|
24
|
-
} else if (token === "-n" || token === "--namespace") {
|
|
25
|
-
if (i + 1 >= tokens.length) return { error: `${token} requires a namespace value.` };
|
|
26
|
-
namespace = tokens[++i];
|
|
27
|
-
} else if (token.startsWith("-n") && token.length > 2 && token[2] !== "-") {
|
|
28
|
-
namespace = token.slice(2);
|
|
29
|
-
} else if (token === "-o" || token === "--output") {
|
|
30
|
-
if (i + 1 >= tokens.length) return { error: `${token} requires a format value (json, yaml, table, wide).` };
|
|
31
|
-
const fmt = tokens[++i];
|
|
32
|
-
if (!VALID_OUTPUT_FORMATS.has(fmt)) {
|
|
33
|
-
return { error: `Invalid output format: "${fmt}". Must be one of: json, yaml, table, wide.` };
|
|
34
|
-
}
|
|
35
|
-
outputFormat = fmt as ParsedResourceArgs["outputFormat"];
|
|
36
|
-
} else if (token.startsWith("-o") && token.length > 2 && token[2] !== "-") {
|
|
37
|
-
const fmt = token.slice(2);
|
|
38
|
-
if (!VALID_OUTPUT_FORMATS.has(fmt)) {
|
|
39
|
-
return { error: `Invalid output format: "${fmt}". Must be one of: json, yaml, table, wide.` };
|
|
40
|
-
}
|
|
41
|
-
outputFormat = fmt as ParsedResourceArgs["outputFormat"];
|
|
42
|
-
} else if (token.startsWith("--dry-run")) {
|
|
43
|
-
if (token.includes("=")) {
|
|
44
|
-
const mode = token.split("=")[1];
|
|
45
|
-
if (!VALID_DRY_RUN_MODES.has(mode)) {
|
|
46
|
-
return { error: `Invalid --dry-run mode: "${mode}". Must be "client" or "server".` };
|
|
47
|
-
}
|
|
48
|
-
dryRun = mode as ParsedResourceArgs["dryRun"];
|
|
49
|
-
} else {
|
|
50
|
-
dryRun = "client";
|
|
51
|
-
}
|
|
52
|
-
} else if (token === "-R" || token === "--recursive") {
|
|
53
|
-
recursive = true;
|
|
54
|
-
} else if (token === "--force") {
|
|
55
|
-
force = true;
|
|
56
|
-
} else if (token.startsWith("-")) {
|
|
57
|
-
return { error: `Unknown flag: "${token}".` };
|
|
58
|
-
} else {
|
|
59
|
-
positionals.push(token);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
filenames,
|
|
65
|
-
namespace,
|
|
66
|
-
outputFormat,
|
|
67
|
-
dryRun,
|
|
68
|
-
recursive,
|
|
69
|
-
force,
|
|
70
|
-
kind: positionals[0],
|
|
71
|
-
name: positionals[1],
|
|
72
|
-
};
|
|
73
|
-
}
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import type { DiffEntry, ResourceDiff } from "./types";
|
|
2
|
-
|
|
3
|
-
export function computeResourceDiff(current: Record<string, unknown>, desired: Record<string, unknown>): ResourceDiff {
|
|
4
|
-
const added: DiffEntry[] = [];
|
|
5
|
-
const removed: DiffEntry[] = [];
|
|
6
|
-
const changed: DiffEntry[] = [];
|
|
7
|
-
let unchangedCount = 0;
|
|
8
|
-
|
|
9
|
-
diffObjects(current, desired, "", added, removed, changed, { count: 0 });
|
|
10
|
-
unchangedCount =
|
|
11
|
-
countLeafKeys(current) + countLeafKeys(desired) - added.length - removed.length - changed.length * 2;
|
|
12
|
-
if (unchangedCount < 0) unchangedCount = 0;
|
|
13
|
-
|
|
14
|
-
return {
|
|
15
|
-
hasDifferences: added.length > 0 || removed.length > 0 || changed.length > 0,
|
|
16
|
-
added,
|
|
17
|
-
removed,
|
|
18
|
-
changed,
|
|
19
|
-
unchangedCount,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function diffObjects(
|
|
24
|
-
current: unknown,
|
|
25
|
-
desired: unknown,
|
|
26
|
-
prefix: string,
|
|
27
|
-
added: DiffEntry[],
|
|
28
|
-
removed: DiffEntry[],
|
|
29
|
-
changed: DiffEntry[],
|
|
30
|
-
_ctx: { count: number },
|
|
31
|
-
): void {
|
|
32
|
-
if (current === desired) return;
|
|
33
|
-
if (current === undefined || current === null) {
|
|
34
|
-
if (desired !== undefined && desired !== null) {
|
|
35
|
-
added.push({ path: prefix || "(root)", newValue: desired });
|
|
36
|
-
}
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
if (desired === undefined || desired === null) {
|
|
40
|
-
removed.push({ path: prefix || "(root)", oldValue: current });
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (Array.isArray(current) && Array.isArray(desired)) {
|
|
45
|
-
diffArrays(current, desired, prefix, added, removed, changed, _ctx);
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
typeof current === "object" &&
|
|
51
|
-
typeof desired === "object" &&
|
|
52
|
-
!Array.isArray(current) &&
|
|
53
|
-
!Array.isArray(desired)
|
|
54
|
-
) {
|
|
55
|
-
const curObj = current as Record<string, unknown>;
|
|
56
|
-
const desObj = desired as Record<string, unknown>;
|
|
57
|
-
const allKeys = new Set([...Object.keys(curObj), ...Object.keys(desObj)]);
|
|
58
|
-
|
|
59
|
-
for (const key of allKeys) {
|
|
60
|
-
const childPath = prefix ? `${prefix}.${key}` : key;
|
|
61
|
-
const curVal = curObj[key];
|
|
62
|
-
const desVal = desObj[key];
|
|
63
|
-
|
|
64
|
-
if (!(key in curObj)) {
|
|
65
|
-
added.push({ path: childPath, newValue: desVal });
|
|
66
|
-
} else if (!(key in desObj)) {
|
|
67
|
-
removed.push({ path: childPath, oldValue: curVal });
|
|
68
|
-
} else {
|
|
69
|
-
diffObjects(curVal, desVal, childPath, added, removed, changed, _ctx);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (!deepEqual(current, desired)) {
|
|
76
|
-
changed.push({ path: prefix || "(root)", oldValue: current, newValue: desired });
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function diffArrays(
|
|
81
|
-
current: unknown[],
|
|
82
|
-
desired: unknown[],
|
|
83
|
-
prefix: string,
|
|
84
|
-
added: DiffEntry[],
|
|
85
|
-
removed: DiffEntry[],
|
|
86
|
-
changed: DiffEntry[],
|
|
87
|
-
_ctx: { count: number },
|
|
88
|
-
): void {
|
|
89
|
-
const maxLen = Math.max(current.length, desired.length);
|
|
90
|
-
for (let i = 0; i < maxLen; i++) {
|
|
91
|
-
const childPath = `${prefix}[${i}]`;
|
|
92
|
-
if (i >= current.length) {
|
|
93
|
-
added.push({ path: childPath, newValue: desired[i] });
|
|
94
|
-
} else if (i >= desired.length) {
|
|
95
|
-
removed.push({ path: childPath, oldValue: current[i] });
|
|
96
|
-
} else {
|
|
97
|
-
diffObjects(current[i], desired[i], childPath, added, removed, changed, _ctx);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function deepEqual(a: unknown, b: unknown): boolean {
|
|
103
|
-
if (a === b) return true;
|
|
104
|
-
if (a == null || b == null) return a === b;
|
|
105
|
-
if (typeof a !== typeof b) return false;
|
|
106
|
-
if (typeof a !== "object") return false;
|
|
107
|
-
|
|
108
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
109
|
-
if (a.length !== b.length) return false;
|
|
110
|
-
return a.every((val, i) => deepEqual(val, b[i]));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (Array.isArray(a) || Array.isArray(b)) return false;
|
|
114
|
-
|
|
115
|
-
const aObj = a as Record<string, unknown>;
|
|
116
|
-
const bObj = b as Record<string, unknown>;
|
|
117
|
-
const aKeys = Object.keys(aObj);
|
|
118
|
-
const bKeys = Object.keys(bObj);
|
|
119
|
-
if (aKeys.length !== bKeys.length) return false;
|
|
120
|
-
return aKeys.every(key => key in bObj && deepEqual(aObj[key], bObj[key]));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function countLeafKeys(obj: unknown): number {
|
|
124
|
-
if (obj == null || typeof obj !== "object") return 1;
|
|
125
|
-
if (Array.isArray(obj)) return obj.reduce((sum, item) => sum + countLeafKeys(item), 0);
|
|
126
|
-
return Object.values(obj as Record<string, unknown>).reduce((sum: number, val) => sum + countLeafKeys(val), 0);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function formatDiff(diff: ResourceDiff, kind: string, name: string): string {
|
|
130
|
-
if (!diff.hasDifferences) {
|
|
131
|
-
return `${kind}/${name}: no changes detected.`;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const lines: string[] = [];
|
|
135
|
-
lines.push(`diff ${kind}/${name}`);
|
|
136
|
-
|
|
137
|
-
for (const entry of diff.removed) {
|
|
138
|
-
lines.push(`- ${entry.path}: ${formatValue(entry.oldValue)}`);
|
|
139
|
-
}
|
|
140
|
-
for (const added of diff.added) {
|
|
141
|
-
lines.push(`+ ${added.path}: ${formatValue(added.newValue)}`);
|
|
142
|
-
}
|
|
143
|
-
for (const entry of diff.changed) {
|
|
144
|
-
lines.push(`~ ${entry.path}: ${formatValue(entry.oldValue)} → ${formatValue(entry.newValue)}`);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
lines.push(` ${diff.unchangedCount} field(s) unchanged`);
|
|
148
|
-
return lines.join("\n");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function formatValue(value: unknown): string {
|
|
152
|
-
if (value === undefined) return "<undefined>";
|
|
153
|
-
if (value === null) return "null";
|
|
154
|
-
if (typeof value === "string") return `"${value}"`;
|
|
155
|
-
if (typeof value === "object") {
|
|
156
|
-
const json = JSON.stringify(value);
|
|
157
|
-
return json.length > 80 ? `${json.slice(0, 77)}...` : json;
|
|
158
|
-
}
|
|
159
|
-
return String(value);
|
|
160
|
-
}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { parseAllDocuments } from "yaml";
|
|
4
|
-
|
|
5
|
-
const JSON_EXTENSIONS = new Set([".json"]);
|
|
6
|
-
const YAML_EXTENSIONS = new Set([".yaml", ".yml"]);
|
|
7
|
-
const SUPPORTED_EXTENSIONS = new Set([...JSON_EXTENSIONS, ...YAML_EXTENSIONS]);
|
|
8
|
-
|
|
9
|
-
export interface FileReadResult {
|
|
10
|
-
objects: Record<string, unknown>[];
|
|
11
|
-
sourcePath: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function readManifestFiles(filenames: string[], recursive: boolean): Promise<FileReadResult[]> {
|
|
15
|
-
const results: FileReadResult[] = [];
|
|
16
|
-
for (const filename of filenames) {
|
|
17
|
-
if (filename === "-") {
|
|
18
|
-
results.push(await readFromStdin());
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const resolved = path.resolve(filename);
|
|
23
|
-
const stat = await fs.promises.stat(resolved).catch(() => null);
|
|
24
|
-
if (!stat) {
|
|
25
|
-
throw new ManifestFileError(`File not found: ${filename}`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (stat.isDirectory()) {
|
|
29
|
-
const files = await collectDirectoryFiles(resolved, recursive);
|
|
30
|
-
for (const file of files) {
|
|
31
|
-
results.push(await readSingleFile(file));
|
|
32
|
-
}
|
|
33
|
-
} else {
|
|
34
|
-
results.push(await readSingleFile(resolved));
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return results;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function readSingleFile(filePath: string): Promise<FileReadResult> {
|
|
41
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
42
|
-
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
43
|
-
|
|
44
|
-
if (JSON_EXTENSIONS.has(ext)) {
|
|
45
|
-
return { objects: parseJsonContent(content, filePath), sourcePath: filePath };
|
|
46
|
-
}
|
|
47
|
-
if (YAML_EXTENSIONS.has(ext)) {
|
|
48
|
-
return { objects: parseYamlContent(content, filePath), sourcePath: filePath };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return { objects: parseAutoDetect(content, filePath), sourcePath: filePath };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function parseJsonContent(content: string, sourcePath: string): Record<string, unknown>[] {
|
|
55
|
-
const trimmed = content.trim();
|
|
56
|
-
if (!trimmed) return [];
|
|
57
|
-
try {
|
|
58
|
-
const parsed = JSON.parse(trimmed);
|
|
59
|
-
if (Array.isArray(parsed)) {
|
|
60
|
-
return parsed.filter((item): item is Record<string, unknown> => item != null && typeof item === "object");
|
|
61
|
-
}
|
|
62
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
63
|
-
return [parsed as Record<string, unknown>];
|
|
64
|
-
}
|
|
65
|
-
throw new ManifestFileError(`Expected object or array in ${sourcePath}, got ${typeof parsed}`);
|
|
66
|
-
} catch (err) {
|
|
67
|
-
if (err instanceof ManifestFileError) throw err;
|
|
68
|
-
throw new ManifestFileError(`Invalid JSON in ${sourcePath}: ${(err as Error).message}`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function parseYamlContent(content: string, sourcePath: string): Record<string, unknown>[] {
|
|
73
|
-
const trimmed = content.trim();
|
|
74
|
-
if (!trimmed) return [];
|
|
75
|
-
try {
|
|
76
|
-
const docs = parseAllDocuments(trimmed);
|
|
77
|
-
const objects: Record<string, unknown>[] = [];
|
|
78
|
-
for (const doc of docs) {
|
|
79
|
-
if (doc.errors.length > 0) {
|
|
80
|
-
const firstError = doc.errors[0];
|
|
81
|
-
throw new ManifestFileError(`Invalid YAML in ${sourcePath}: ${firstError.message}`);
|
|
82
|
-
}
|
|
83
|
-
const value = doc.toJSON();
|
|
84
|
-
if (value != null && typeof value === "object" && !Array.isArray(value)) {
|
|
85
|
-
objects.push(value as Record<string, unknown>);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return objects;
|
|
89
|
-
} catch (err) {
|
|
90
|
-
if (err instanceof ManifestFileError) throw err;
|
|
91
|
-
throw new ManifestFileError(`Invalid YAML in ${sourcePath}: ${(err as Error).message}`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function parseAutoDetect(content: string, sourcePath: string): Record<string, unknown>[] {
|
|
96
|
-
const trimmed = content.trim();
|
|
97
|
-
if (!trimmed) return [];
|
|
98
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
99
|
-
return parseJsonContent(content, sourcePath);
|
|
100
|
-
}
|
|
101
|
-
return parseYamlContent(content, sourcePath);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function collectDirectoryFiles(dirPath: string, recursive: boolean): Promise<string[]> {
|
|
105
|
-
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
106
|
-
const files: string[] = [];
|
|
107
|
-
|
|
108
|
-
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
109
|
-
const fullPath = path.join(dirPath, entry.name);
|
|
110
|
-
if (entry.isFile()) {
|
|
111
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
112
|
-
if (SUPPORTED_EXTENSIONS.has(ext)) {
|
|
113
|
-
files.push(fullPath);
|
|
114
|
-
}
|
|
115
|
-
} else if (entry.isDirectory() && recursive) {
|
|
116
|
-
files.push(...(await collectDirectoryFiles(fullPath, recursive)));
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return files;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function readFromStdin(): Promise<FileReadResult> {
|
|
123
|
-
return new Promise((resolve, reject) => {
|
|
124
|
-
const chunks: Buffer[] = [];
|
|
125
|
-
let timedOut = false;
|
|
126
|
-
|
|
127
|
-
const timer = setTimeout(() => {
|
|
128
|
-
timedOut = true;
|
|
129
|
-
if (chunks.length === 0) {
|
|
130
|
-
reject(new ManifestFileError("No data received from stdin (timed out after 5s)."));
|
|
131
|
-
}
|
|
132
|
-
}, 5000);
|
|
133
|
-
|
|
134
|
-
process.stdin.on("data", (chunk: Buffer) => {
|
|
135
|
-
chunks.push(chunk);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
process.stdin.on("end", () => {
|
|
139
|
-
clearTimeout(timer);
|
|
140
|
-
if (timedOut && chunks.length === 0) return;
|
|
141
|
-
const content = Buffer.concat(chunks).toString("utf-8");
|
|
142
|
-
try {
|
|
143
|
-
resolve({ objects: parseAutoDetect(content, "stdin"), sourcePath: "stdin" });
|
|
144
|
-
} catch (err) {
|
|
145
|
-
reject(err);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
process.stdin.on("error", err => {
|
|
150
|
-
clearTimeout(timer);
|
|
151
|
-
reject(new ManifestFileError(`Failed to read from stdin: ${err.message}`));
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
process.stdin.resume();
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export class ManifestFileError extends Error {
|
|
159
|
-
constructor(message: string) {
|
|
160
|
-
super(message);
|
|
161
|
-
this.name = "ManifestFileError";
|
|
162
|
-
}
|
|
163
|
-
}
|