@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "19.30.0",
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.0",
54
- "@f5xc-salesdemos/pi-agent-core": "19.30.0",
55
- "@f5xc-salesdemos/pi-ai": "19.30.0",
56
- "@f5xc-salesdemos/pi-natives": "19.30.0",
57
- "@f5xc-salesdemos/pi-tui": "19.30.0",
58
- "@f5xc-salesdemos/pi-utils": "19.30.0",
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.0",
21
- "commit": "06f9364382e7e1bbc13059da2380ce61008f76ba",
22
- "shortCommit": "06f9364",
20
+ "version": "19.30.1",
21
+ "commit": "b1150372a182f6653cf5a62a53edb7d1c04c277a",
22
+ "shortCommit": "b115037",
23
23
  "branch": "main",
24
- "tag": "v19.30.0",
25
- "commitDate": "2026-06-14T20:22:36Z",
26
- "buildDate": "2026-06-14T20:42:36.884Z",
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/06f9364382e7e1bbc13059da2380ce61008f76ba",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.30.0"
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
- export { parseResourceArgs } from "./arg-parser";
2
- export { computeResourceDiff, formatDiff } from "./diff-engine";
3
- export { ManifestFileError, readManifestFiles } from "./file-reader";
4
- export { getAllKnownKinds, getKindsWithApiPaths, KindResolutionError, resolveKind } from "./kind-resolver";
5
- export { ManifestParseError, parseManifests } from "./manifest-parser";
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
- } from "./types";
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 { getKindsWithApiPaths } =
13
- require("../resource-management/kind-resolver") as typeof import("../resource-management/kind-resolver");
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 { parseResourceArgs } = await import("../resource-management/arg-parser");
35
- const parsed = parseResourceArgs(command.args);
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
- }