-
-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathcli.ts
More file actions
executable file
·220 lines (173 loc) · 5.93 KB
/
cli.ts
File metadata and controls
executable file
·220 lines (173 loc) · 5.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
#!/usr/bin/env tsx
import fs from 'node:fs/promises'
import path from 'node:path'
import { CoTParser, DataPackage } from './index.js'
type CommandHandler = (args: string[]) => Promise<void>
interface PackageIssue {
entry: string;
uid?: string;
error: string;
}
function usage(): string {
return [
'Usage:',
' ./cli.ts package validate <file>.zip',
' ./cli.ts feature convert <input.geojson|input.cot>'
].join('\n')
}
function getErrorMessage(err: unknown): string {
if (err instanceof Error) return err.message;
return String(err);
}
async function withoutConsoleWarn<T>(fn: () => Promise<T> | T): Promise<T> {
const warn = console.warn;
console.warn = () => undefined;
try {
return await fn();
} finally {
console.warn = warn;
}
}
function extractUID(raw: string): string | undefined {
const match = raw.match(/<event\b[^>]*\buid=(['"])([^'"]+)\1/i);
return match?.[2];
}
function isJSONInput(inputPath: string, raw: string): boolean {
const ext = path.extname(inputPath).toLowerCase();
if (ext === '.geojson' || ext === '.json') return true;
return raw.trimStart().startsWith('{');
}
function normalizeGeoJSON(input: unknown): Record<string, unknown> {
if (!input || typeof input !== 'object' || Array.isArray(input)) {
throw new Error('GeoJSON input must be an object');
}
const geojson = input as Record<string, unknown>;
if (geojson.type === 'Feature') {
return geojson;
}
if (geojson.type === 'FeatureCollection') {
const features = geojson.features;
if (!Array.isArray(features) || features.length !== 1) {
throw new Error('FeatureCollection input must contain exactly one feature');
}
const [feature] = features;
if (!feature || typeof feature !== 'object' || Array.isArray(feature)) {
throw new Error('FeatureCollection contains an invalid feature');
}
return feature as Record<string, unknown>;
}
throw new Error('GeoJSON input must be a Feature or a single-feature FeatureCollection');
}
async function validatePackage(inputPath: string): Promise<void> {
const resolved = path.resolve(inputPath);
let pkg: DataPackage | undefined;
try {
pkg = await withoutConsoleWarn(async () => {
return await DataPackage.parse(resolved, {
cleanup: false,
strict: true
});
});
const issues: PackageIssue[] = [];
for (const content of pkg.contents) {
const entry = content._attributes.zipEntry;
const buffer = await pkg.getFileBuffer(entry);
if (path.extname(entry).toLowerCase() !== '.cot') continue;
const raw = buffer.toString('utf8');
try {
await withoutConsoleWarn(() => CoTParser.from_xml(raw));
} catch (err) {
issues.push({
entry,
uid: extractUID(raw),
error: getErrorMessage(err)
});
}
}
if (issues.length) {
console.error(JSON.stringify({
valid: false,
package: resolved,
errors: issues
}, null, 4));
process.exitCode = 1;
return;
}
const cots = await pkg.cots({
respectIgnore: false,
parseAttachments: true
});
const attachments = await pkg.attachments({
respectIgnore: false
});
const files = await pkg.files({
respectIgnore: false
});
const attachmentCount = Array.from(attachments.values())
.reduce((sum, entries) => sum + entries.length, 0);
console.log(JSON.stringify({
valid: true,
package: resolved,
manifest: pkg.settings,
counts: {
contents: pkg.contents.length,
cots: cots.length,
attachments: attachmentCount,
files: files.size
}
}, null, 4));
} finally {
if (pkg) {
await pkg.destroy();
}
}
}
async function convertFeature(inputPath: string): Promise<void> {
const resolved = path.resolve(inputPath);
const raw = await fs.readFile(resolved, 'utf8');
if (isJSONInput(resolved, raw)) {
const feature = normalizeGeoJSON(JSON.parse(raw));
const cot = await withoutConsoleWarn(async () => {
return await CoTParser.from_geojson(feature as never);
});
const xml = CoTParser.to_xml(cot);
await withoutConsoleWarn(() => CoTParser.from_xml(xml));
process.stdout.write(`${xml}\n`);
return;
}
const cot = await withoutConsoleWarn(() => CoTParser.from_xml(raw));
const feature = await CoTParser.to_geojson(cot);
process.stdout.write(`${JSON.stringify(feature, null, 4)}\n`);
}
const commands: Record<string, CommandHandler> = {
'package validate': async (args) => {
if (args.length !== 1) {
throw new Error('package validate requires a single .zip file path');
}
await validatePackage(args[0]);
},
'feature convert': async (args) => {
if (args.length !== 1) {
throw new Error('feature convert requires a single input path');
}
await convertFeature(args[0]);
}
}
try {
const args = process.argv.slice(2);
if (!args.length || args.includes('--help') || args.includes('-h')) {
console.log(usage());
process.exit(0);
}
const commandKey = args.slice(0, 2).join(' ');
const handler = commands[commandKey];
if (!handler) {
throw new Error(`Unknown command: ${args.join(' ')}`);
}
await handler(args.slice(2));
} catch (err) {
console.error(getErrorMessage(err));
console.error('');
console.error(usage());
process.exit(1);
}