@ldsg/cli 0.3.28 → 0.3.30
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/.rush/temp/package-deps_build.json +22 -5
- package/.tsbuildinfo +1 -1
- package/CHANGELOG.json +52 -0
- package/CHANGELOG.md +15 -1
- package/dist/__tests__/commands.test.d.ts +1 -0
- package/dist/__tests__/commands.test.js +219 -0
- package/dist/__tests__/commands.test.js.map +1 -0
- package/dist/__tests__/formatter.test.d.ts +1 -0
- package/dist/__tests__/formatter.test.js +114 -0
- package/dist/__tests__/formatter.test.js.map +1 -0
- package/dist/__tests__/json-file-mutator.test.d.ts +1 -0
- package/dist/__tests__/json-file-mutator.test.js +103 -0
- package/dist/__tests__/json-file-mutator.test.js.map +1 -0
- package/dist/__tests__/source-resolver.test.d.ts +1 -0
- package/dist/__tests__/source-resolver.test.js +101 -0
- package/dist/__tests__/source-resolver.test.js.map +1 -0
- package/dist/cli.js +304 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/resource/convert.d.ts +11 -0
- package/dist/commands/resource/convert.js +42 -0
- package/dist/commands/resource/convert.js.map +1 -0
- package/dist/commands/resource/create.d.ts +14 -0
- package/dist/commands/resource/create.js +37 -0
- package/dist/commands/resource/create.js.map +1 -0
- package/dist/commands/resource/delete.d.ts +10 -0
- package/dist/commands/resource/delete.js +10 -0
- package/dist/commands/resource/delete.js.map +1 -0
- package/dist/commands/resource/diff.d.ts +10 -0
- package/dist/commands/resource/diff.js +72 -0
- package/dist/commands/resource/diff.js.map +1 -0
- package/dist/commands/resource/get.d.ts +6 -0
- package/dist/commands/resource/get.js +6 -0
- package/dist/commands/resource/get.js.map +1 -0
- package/dist/commands/resource/list.d.ts +30 -0
- package/dist/commands/resource/list.js +138 -0
- package/dist/commands/resource/list.js.map +1 -0
- package/dist/commands/resource/tree.d.ts +2 -0
- package/dist/commands/resource/tree.js +6 -0
- package/dist/commands/resource/tree.js.map +1 -0
- package/dist/commands/resource/update.d.ts +11 -0
- package/dist/commands/resource/update.js +13 -0
- package/dist/commands/resource/update.js.map +1 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +3 -1
- package/dist/constants.js.map +1 -1
- package/dist/types/resource-record.d.ts +43 -0
- package/dist/types/resource-record.js +3 -0
- package/dist/types/resource-record.js.map +1 -0
- package/dist/utils/formatter.d.ts +12 -0
- package/dist/utils/formatter.js +96 -0
- package/dist/utils/formatter.js.map +1 -0
- package/dist/utils/json-file-mutator.d.ts +15 -0
- package/dist/utils/json-file-mutator.js +109 -0
- package/dist/utils/json-file-mutator.js.map +1 -0
- package/dist/utils/source-resolver.d.ts +4 -0
- package/dist/utils/source-resolver.js +93 -0
- package/dist/utils/source-resolver.js.map +1 -0
- package/jest.config.js +12 -0
- package/package.json +8 -6
- package/src/__tests__/commands.test.ts +262 -0
- package/src/__tests__/formatter.test.ts +133 -0
- package/src/__tests__/json-file-mutator.test.ts +136 -0
- package/src/__tests__/source-resolver.test.ts +125 -0
- package/src/cli.ts +309 -1
- package/src/commands/resource/convert.ts +52 -0
- package/src/commands/resource/create.ts +49 -0
- package/src/commands/resource/delete.ts +15 -0
- package/src/commands/resource/diff.ts +104 -0
- package/src/commands/resource/get.ts +5 -0
- package/src/commands/resource/list.ts +161 -0
- package/src/commands/resource/tree.ts +4 -0
- package/src/commands/resource/update.ts +21 -0
- package/src/constants.ts +3 -0
- package/src/types/resource-record.ts +52 -0
- package/src/utils/formatter.ts +106 -0
- package/src/utils/json-file-mutator.ts +137 -0
- package/src/utils/source-resolver.ts +102 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { ResourceRecord, ResourceRecords } from '@ldsg/types';
|
|
2
|
+
import { ResourceFilter } from '../../types/resource-record';
|
|
3
|
+
|
|
4
|
+
function filterByKind(records: ResourceRecords, kind?: string): ResourceRecords {
|
|
5
|
+
if (!kind) return records;
|
|
6
|
+
return records.filter((r) => r.kind === kind);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function filterByParentId(records: ResourceRecords, parentId?: string): ResourceRecords {
|
|
10
|
+
if (parentId === undefined || parentId === null) return records;
|
|
11
|
+
return records.filter((r) => r.parentId === parentId);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getDescendantIds(allRecords: ResourceRecords, rootId: string): Set<string> {
|
|
15
|
+
const childMap = new Map<string, string[]>();
|
|
16
|
+
for (const r of allRecords) {
|
|
17
|
+
const pid = r.parentId || 'null';
|
|
18
|
+
if (!childMap.has(pid)) childMap.set(pid, []);
|
|
19
|
+
childMap.get(pid)!.push(r.id);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const result = new Set<string>();
|
|
23
|
+
const queue = [rootId];
|
|
24
|
+
while (queue.length > 0) {
|
|
25
|
+
const id = queue.shift()!;
|
|
26
|
+
if (result.has(id)) continue;
|
|
27
|
+
result.add(id);
|
|
28
|
+
const children = childMap.get(id) || [];
|
|
29
|
+
for (const childId of children) {
|
|
30
|
+
queue.push(childId);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function filterByRootId(records: ResourceRecords, rootId?: string): ResourceRecords {
|
|
37
|
+
if (!rootId) return records;
|
|
38
|
+
const ids = getDescendantIds(records, rootId);
|
|
39
|
+
return records.filter((r) => ids.has(r.id));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildChildMap(records: ResourceRecords): Map<string, ResourceRecord[]> {
|
|
43
|
+
const map = new Map<string, ResourceRecord[]>();
|
|
44
|
+
for (const r of records) {
|
|
45
|
+
const pid = r.parentId || 'null';
|
|
46
|
+
if (!map.has(pid)) map.set(pid, []);
|
|
47
|
+
map.get(pid)!.push(r);
|
|
48
|
+
}
|
|
49
|
+
return map;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function findRoots(records: ResourceRecords): ResourceRecord[] {
|
|
53
|
+
const roots = records.filter((r) => r.parentId === null || r.parentId === 'root');
|
|
54
|
+
if (roots.length > 0) return roots;
|
|
55
|
+
const allParentIds = new Set(records.map((r) => r.parentId).filter(Boolean));
|
|
56
|
+
return records.filter((r) => !allParentIds.has(r.id));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildTree(
|
|
60
|
+
records: ResourceRecords,
|
|
61
|
+
rootId: string
|
|
62
|
+
): { resource: ResourceRecord; children: unknown[] } | null {
|
|
63
|
+
const root = records.find((r) => r.id === rootId);
|
|
64
|
+
if (!root) return null;
|
|
65
|
+
|
|
66
|
+
const childMap = buildChildMap(records);
|
|
67
|
+
|
|
68
|
+
function buildSubTree(nodeId: string): { resource: ResourceRecord; children: unknown[] } {
|
|
69
|
+
const node = records.find((r) => r.id === nodeId)!;
|
|
70
|
+
const children = (childMap.get(nodeId) || []).map((child) => buildSubTree(child.id));
|
|
71
|
+
return { resource: node, children };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return buildSubTree(rootId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function listResources(
|
|
78
|
+
records: ResourceRecords,
|
|
79
|
+
filter: ResourceFilter
|
|
80
|
+
): Promise<{
|
|
81
|
+
total: number;
|
|
82
|
+
resources: ResourceRecords;
|
|
83
|
+
}> {
|
|
84
|
+
let result = records;
|
|
85
|
+
|
|
86
|
+
result = filterByKind(result, filter.kind);
|
|
87
|
+
result = filterByParentId(result, filter.parentId);
|
|
88
|
+
result = filterByRootId(result, filter.rootId);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
total: result.length,
|
|
92
|
+
resources: result
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function getResource(
|
|
97
|
+
records: ResourceRecords,
|
|
98
|
+
id: string,
|
|
99
|
+
options: { children?: boolean; tree?: boolean }
|
|
100
|
+
): Promise<ResourceRecord | (ResourceRecord & { children?: ResourceRecords; tree?: unknown }) | null> {
|
|
101
|
+
const record = records.find((r) => r.id === id);
|
|
102
|
+
if (!record) return null;
|
|
103
|
+
|
|
104
|
+
if (options.tree) {
|
|
105
|
+
const tree = buildTree(records, id);
|
|
106
|
+
return { ...record, tree: tree ? tree : null };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (options.children) {
|
|
110
|
+
const children = records.filter((r) => r.parentId === id);
|
|
111
|
+
return { ...record, children };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return record;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function getTree(
|
|
118
|
+
records: ResourceRecords,
|
|
119
|
+
rootId?: string,
|
|
120
|
+
showKind?: boolean
|
|
121
|
+
): Promise<{ rootId: string; tree: { resource: ResourceRecord; children: unknown[] } }> {
|
|
122
|
+
let effectiveRootId = rootId;
|
|
123
|
+
|
|
124
|
+
if (!effectiveRootId) {
|
|
125
|
+
const roots = findRoots(records);
|
|
126
|
+
if (roots.length === 0) {
|
|
127
|
+
throw new Error('No root resource found. Specify a root-id.');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (roots.length === 1) {
|
|
131
|
+
effectiveRootId = roots[0].id;
|
|
132
|
+
} else {
|
|
133
|
+
effectiveRootId = '__virtual_root__';
|
|
134
|
+
const virtualRoot: ResourceRecord = {
|
|
135
|
+
id: '__virtual_root__',
|
|
136
|
+
kind: 'virtual',
|
|
137
|
+
parentId: null,
|
|
138
|
+
settings: { title: 'Virtual Root' }
|
|
139
|
+
};
|
|
140
|
+
const childMap = buildChildMap(records);
|
|
141
|
+
const children = roots.map((r) => buildTree(records, r.id)!).filter(Boolean);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
rootId: effectiveRootId,
|
|
145
|
+
tree: {
|
|
146
|
+
resource: virtualRoot,
|
|
147
|
+
children: showKind ? children : children
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const tree = buildTree(records, effectiveRootId);
|
|
154
|
+
if (!tree) {
|
|
155
|
+
throw new Error(`Resource with id "${effectiveRootId}" not found`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { rootId: effectiveRootId, tree };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export { filterByKind, filterByParentId, filterByRootId, buildChildMap, findRoots, buildTree };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ResourceRecord } from '@ldsg/types';
|
|
2
|
+
import { SourceData } from '../../types/resource-record';
|
|
3
|
+
import { updateRecord } from '../../utils/json-file-mutator';
|
|
4
|
+
|
|
5
|
+
export interface UpdateOptions {
|
|
6
|
+
sourceData: SourceData;
|
|
7
|
+
id: string;
|
|
8
|
+
settings: Record<string, unknown>;
|
|
9
|
+
dryRun?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function updateResource(options: UpdateOptions): Promise<{ record: ResourceRecord }> {
|
|
13
|
+
const { sourceData, id, settings, dryRun } = options;
|
|
14
|
+
|
|
15
|
+
if (!settings || Object.keys(settings).length === 0) {
|
|
16
|
+
throw new Error('Missing required option: --settings');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const result = await updateRecord(sourceData, id, settings, dryRun);
|
|
20
|
+
return { record: result.record };
|
|
21
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ResourceRecord, ResourceRecords } from '@ldsg/types';
|
|
2
|
+
|
|
3
|
+
export interface CliError {
|
|
4
|
+
code: string;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface CliResponse<T = unknown> {
|
|
9
|
+
success: boolean;
|
|
10
|
+
data?: T;
|
|
11
|
+
error?: CliError;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ResourceFilter {
|
|
15
|
+
kind?: string;
|
|
16
|
+
parentId?: string;
|
|
17
|
+
rootId?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TreeNode {
|
|
21
|
+
resource: ResourceRecord;
|
|
22
|
+
children: TreeNode[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DiffResultField {
|
|
26
|
+
path: string;
|
|
27
|
+
from?: unknown;
|
|
28
|
+
to?: unknown;
|
|
29
|
+
value?: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DiffResult {
|
|
33
|
+
id: string;
|
|
34
|
+
kind: string;
|
|
35
|
+
sourceA: string;
|
|
36
|
+
sourceB: string;
|
|
37
|
+
changed: DiffResultField[];
|
|
38
|
+
added: DiffResultField[];
|
|
39
|
+
removed: DiffResultField[];
|
|
40
|
+
sideA: ResourceRecord | null;
|
|
41
|
+
sideB: ResourceRecord | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type SourceFormat = 'array' | 'object';
|
|
45
|
+
|
|
46
|
+
export interface SourceData {
|
|
47
|
+
resourceRecords: ResourceRecords;
|
|
48
|
+
format: SourceFormat;
|
|
49
|
+
filePath: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { ResourceRecord, ResourceRecords };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { CliResponse, TreeNode, DiffResult } from '../types/resource-record';
|
|
2
|
+
|
|
3
|
+
export function formatSuccess<T>(data: T): CliResponse<T> {
|
|
4
|
+
return { success: true, data };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function formatError(code: string, message: string): CliResponse<never> {
|
|
8
|
+
return { success: false, error: { code, message } };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function formatJson(data: unknown): string {
|
|
12
|
+
return JSON.stringify(data, null, 2);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatValue(value: unknown): string {
|
|
16
|
+
if (value === null || value === undefined) {
|
|
17
|
+
return 'null';
|
|
18
|
+
}
|
|
19
|
+
if (typeof value === 'string') {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === 'object') {
|
|
23
|
+
return JSON.stringify(value);
|
|
24
|
+
}
|
|
25
|
+
return String(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function formatPrettyList(
|
|
29
|
+
resources: { id: string; kind: string; parentId: string | null; title?: string }[]
|
|
30
|
+
): string {
|
|
31
|
+
if (resources.length === 0) {
|
|
32
|
+
return 'No resources found.';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const lines: string[] = [];
|
|
36
|
+
lines.push(`${'ID'.padEnd(30)} ${'KIND'.padEnd(20)} PARENT ID`);
|
|
37
|
+
lines.push('-'.repeat(70));
|
|
38
|
+
|
|
39
|
+
for (const r of resources) {
|
|
40
|
+
const title = r.title || '';
|
|
41
|
+
const displayId = title ? `${r.id} (${title})` : r.id;
|
|
42
|
+
lines.push(`${displayId.padEnd(30)} ${r.kind.padEnd(20)} ${r.parentId || '-'}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
lines.push('');
|
|
46
|
+
lines.push(`Total: ${resources.length} resource(s)`);
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function formatPrettyTree(node: TreeNode, showKind: boolean, indent = ''): string {
|
|
51
|
+
const prefix = indent ? indent.slice(0, -4) + (indent.endsWith('└── ') ? ' ' : '│ ') : '';
|
|
52
|
+
const label = showKind ? `[${node.resource.kind}] ${node.resource.id}` : node.resource.id;
|
|
53
|
+
|
|
54
|
+
if (indent === '') {
|
|
55
|
+
const lines: string[] = [label];
|
|
56
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
57
|
+
const child = node.children[i];
|
|
58
|
+
const connector = i === node.children.length - 1 ? '└── ' : '├── ';
|
|
59
|
+
lines.push(formatPrettyTree(child, showKind, connector));
|
|
60
|
+
}
|
|
61
|
+
return lines.join('\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const result = `${indent}${label}`;
|
|
65
|
+
const childLines: string[] = [];
|
|
66
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
67
|
+
const child = node.children[i];
|
|
68
|
+
const connector = i === node.children.length - 1 ? '└── ' : '├── ';
|
|
69
|
+
childLines.push(formatPrettyTree(child, showKind, `${prefix}${connector}`));
|
|
70
|
+
}
|
|
71
|
+
return [result, ...childLines].join('\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function formatPrettyDiff(diff: DiffResult): string {
|
|
75
|
+
const lines: string[] = [];
|
|
76
|
+
lines.push(`Resource: ${diff.id} (${diff.kind})`);
|
|
77
|
+
lines.push(`Source A: ${diff.sourceA}`);
|
|
78
|
+
lines.push(`Source B: ${diff.sourceB}`);
|
|
79
|
+
lines.push('');
|
|
80
|
+
|
|
81
|
+
if (diff.sideA === null) {
|
|
82
|
+
lines.push('⚠ Resource only exists in source B (new)');
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
}
|
|
85
|
+
if (diff.sideB === null) {
|
|
86
|
+
lines.push('⚠ Resource only exists in source A (removed)');
|
|
87
|
+
return lines.join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (diff.changed.length === 0 && diff.added.length === 0 && diff.removed.length === 0) {
|
|
91
|
+
lines.push('No differences found.');
|
|
92
|
+
return lines.join('\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const field of diff.changed) {
|
|
96
|
+
lines.push(` ${field.path}: "${formatValue(field.from)}" → "${formatValue(field.to)}"`);
|
|
97
|
+
}
|
|
98
|
+
for (const field of diff.added) {
|
|
99
|
+
lines.push(` ${field.path}: +"${formatValue(field.value)}"`);
|
|
100
|
+
}
|
|
101
|
+
for (const field of diff.removed) {
|
|
102
|
+
lines.push(` ${field.path}: -"${formatValue(field.from)}"`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return lines.join('\n');
|
|
106
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { ResourceRecord, ResourceRecords } from '@ldsg/types';
|
|
3
|
+
import { SourceData } from '../types/resource-record';
|
|
4
|
+
|
|
5
|
+
function buildChildMap(resourceRecords: ResourceRecords): Map<string, ResourceRecord[]> {
|
|
6
|
+
const childMap = new Map<string, ResourceRecord[]>();
|
|
7
|
+
for (const record of resourceRecords) {
|
|
8
|
+
const pid = record.parentId || 'null';
|
|
9
|
+
const children = childMap.get(pid) || [];
|
|
10
|
+
children.push(record);
|
|
11
|
+
childMap.set(pid, children);
|
|
12
|
+
}
|
|
13
|
+
return childMap;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getDescendantIds(resourceRecords: ResourceRecords, id: string): string[] {
|
|
17
|
+
const childMap = buildChildMap(resourceRecords);
|
|
18
|
+
const ids: string[] = [];
|
|
19
|
+
const queue = [id];
|
|
20
|
+
while (queue.length > 0) {
|
|
21
|
+
const currentId = queue.shift()!;
|
|
22
|
+
ids.push(currentId);
|
|
23
|
+
const children = childMap.get(currentId) || [];
|
|
24
|
+
for (const child of children) {
|
|
25
|
+
queue.push(child.id);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return ids;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function extractSourceData(data: unknown): { records: ResourceRecords; isArray: boolean } {
|
|
32
|
+
if (Array.isArray(data)) {
|
|
33
|
+
return { records: data as ResourceRecords, isArray: true };
|
|
34
|
+
}
|
|
35
|
+
if (data && typeof data === 'object') {
|
|
36
|
+
const obj = data as Record<string, unknown>;
|
|
37
|
+
if (Array.isArray(obj.resourceRecords)) {
|
|
38
|
+
return { records: obj.resourceRecords as ResourceRecords, isArray: false };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
throw new Error('Invalid resource records format');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildOutputData(records: ResourceRecords, isArray: boolean): unknown {
|
|
45
|
+
if (isArray) {
|
|
46
|
+
return records;
|
|
47
|
+
}
|
|
48
|
+
return { resourceRecords: records };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function readSourceFile(filePath: string): Promise<{ records: ResourceRecords; isArray: boolean }> {
|
|
52
|
+
const data = await fs.readJson(filePath);
|
|
53
|
+
return extractSourceData(data);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function writeSourceFile(filePath: string, records: ResourceRecords, isArray: boolean): Promise<void> {
|
|
57
|
+
const output = buildOutputData(records, isArray);
|
|
58
|
+
await fs.writeJson(filePath, output, { spaces: 2 });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function generateId(): string {
|
|
62
|
+
const timestamp = Date.now().toString(36);
|
|
63
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
64
|
+
return `${timestamp}-${random}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function appendRecord(
|
|
68
|
+
sourceData: SourceData,
|
|
69
|
+
record: ResourceRecord,
|
|
70
|
+
dryRun = false
|
|
71
|
+
): Promise<{ record: ResourceRecord; allRecords: ResourceRecords }> {
|
|
72
|
+
const { records, isArray } = await readSourceFile(sourceData.filePath);
|
|
73
|
+
|
|
74
|
+
const existingIndex = records.findIndex((r) => r.id === record.id);
|
|
75
|
+
if (existingIndex >= 0) {
|
|
76
|
+
throw new Error(`Resource with id "${record.id}" already exists`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const allRecords = [...records, record];
|
|
80
|
+
|
|
81
|
+
if (!dryRun) {
|
|
82
|
+
await writeSourceFile(sourceData.filePath, allRecords, isArray);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { record, allRecords };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function updateRecord(
|
|
89
|
+
sourceData: SourceData,
|
|
90
|
+
id: string,
|
|
91
|
+
settings: Record<string, unknown>,
|
|
92
|
+
dryRun = false
|
|
93
|
+
): Promise<{ record: ResourceRecord; allRecords: ResourceRecords }> {
|
|
94
|
+
const { records, isArray } = await readSourceFile(sourceData.filePath);
|
|
95
|
+
|
|
96
|
+
const index = records.findIndex((r) => r.id === id);
|
|
97
|
+
if (index < 0) {
|
|
98
|
+
throw new Error(`Resource with id "${id}" not found`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const updatedRecord: ResourceRecord = {
|
|
102
|
+
...records[index],
|
|
103
|
+
settings: { ...records[index].settings, ...settings }
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const allRecords = [...records];
|
|
107
|
+
allRecords[index] = updatedRecord;
|
|
108
|
+
|
|
109
|
+
if (!dryRun) {
|
|
110
|
+
await writeSourceFile(sourceData.filePath, allRecords, isArray);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { record: updatedRecord, allRecords };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function removeRecord(
|
|
117
|
+
sourceData: SourceData,
|
|
118
|
+
id: string,
|
|
119
|
+
dryRun = false
|
|
120
|
+
): Promise<{ removed: ResourceRecord[]; allRecords: ResourceRecords }> {
|
|
121
|
+
const { records, isArray } = await readSourceFile(sourceData.filePath);
|
|
122
|
+
|
|
123
|
+
const targetIndex = records.findIndex((r) => r.id === id);
|
|
124
|
+
if (targetIndex < 0) {
|
|
125
|
+
throw new Error(`Resource with id "${id}" not found`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const idsToRemove = new Set(getDescendantIds(records, id));
|
|
129
|
+
const removed = records.filter((r) => idsToRemove.has(r.id));
|
|
130
|
+
const allRecords = records.filter((r) => !idsToRemove.has(r.id));
|
|
131
|
+
|
|
132
|
+
if (!dryRun) {
|
|
133
|
+
await writeSourceFile(sourceData.filePath, allRecords, isArray);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { removed, allRecords };
|
|
137
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { ResourceRecords } from '@ldsg/types';
|
|
3
|
+
import { DEFAULT_SOURCE_FILE } from '../constants';
|
|
4
|
+
import { SourceData, SourceFormat } from '../types/resource-record';
|
|
5
|
+
|
|
6
|
+
async function readJsonFile(filePath: string): Promise<unknown> {
|
|
7
|
+
if (!(await fs.pathExists(filePath))) {
|
|
8
|
+
throw new Error(`File not found: ${filePath}`);
|
|
9
|
+
}
|
|
10
|
+
return fs.readJson(filePath);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseResourceRecords(data: unknown): { resourceRecords: ResourceRecords; format: SourceFormat } {
|
|
14
|
+
if (Array.isArray(data)) {
|
|
15
|
+
return { resourceRecords: data as ResourceRecords, format: 'array' };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (data && typeof data === 'object') {
|
|
19
|
+
const obj = data as Record<string, unknown>;
|
|
20
|
+
if (Array.isArray(obj.resourceRecords)) {
|
|
21
|
+
return { resourceRecords: obj.resourceRecords as ResourceRecords, format: 'object' };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
throw new Error('Invalid resource records format: expected an array or { resourceRecords: [] }');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function resolveFile(filePath: string): Promise<SourceData> {
|
|
29
|
+
const data = await readJsonFile(filePath);
|
|
30
|
+
const { resourceRecords, format } = parseResourceRecords(data);
|
|
31
|
+
return { resourceRecords, format, filePath };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function resolveDirectory(dirPath: string): Promise<SourceData> {
|
|
35
|
+
const filePath = `${dirPath.replace(/\/$/, '')}/${DEFAULT_SOURCE_FILE}`;
|
|
36
|
+
return resolveFile(filePath);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function resolveStdin(): Promise<SourceData> {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const chunks: Buffer[] = [];
|
|
42
|
+
process.stdin.setEncoding('utf-8');
|
|
43
|
+
|
|
44
|
+
process.stdin.on('data', (chunk: string | Buffer) => {
|
|
45
|
+
chunks.push(Buffer.from(chunk));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
process.stdin.on('end', () => {
|
|
49
|
+
try {
|
|
50
|
+
const raw = Buffer.concat(chunks).toString('utf-8');
|
|
51
|
+
const data = JSON.parse(raw);
|
|
52
|
+
const { resourceRecords, format } = parseResourceRecords(data);
|
|
53
|
+
resolve({ resourceRecords, format, filePath: '-stdin-' });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
reject(new Error(`Failed to parse stdin as JSON: ${(error as Error).message}`));
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
process.stdin.on('error', (error) => {
|
|
60
|
+
reject(new Error(`Failed to read stdin: ${error.message}`));
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function resolveSource(source?: string): Promise<SourceData> {
|
|
66
|
+
if (!source || source === DEFAULT_SOURCE_FILE) {
|
|
67
|
+
const defaultPath = `./${DEFAULT_SOURCE_FILE}`;
|
|
68
|
+
if (await fs.pathExists(defaultPath)) {
|
|
69
|
+
return resolveFile(defaultPath);
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`Default source file not found: ${defaultPath}. Use --source to specify a file.`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (source === '-') {
|
|
75
|
+
return resolveStdin();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const stats = await fs.stat(source).catch(() => null);
|
|
79
|
+
if (!stats) {
|
|
80
|
+
throw new Error(`Source not found: ${source}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (stats.isDirectory()) {
|
|
84
|
+
return resolveDirectory(source);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return resolveFile(source);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function isStdinSource(source?: string): boolean {
|
|
91
|
+
return source === '-';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getSourceDescription(source?: string): string {
|
|
95
|
+
if (!source || source === DEFAULT_SOURCE_FILE) {
|
|
96
|
+
return `./${DEFAULT_SOURCE_FILE}`;
|
|
97
|
+
}
|
|
98
|
+
if (source === '-') {
|
|
99
|
+
return 'stdin';
|
|
100
|
+
}
|
|
101
|
+
return source;
|
|
102
|
+
}
|