@polytric/openws-sdkgen 0.0.2 → 0.0.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/dist/main.cjs +426 -0
- package/dist/main.d.cts +1 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +403 -0
- package/dist/plans/dotnet.cjs +229 -0
- package/dist/plans/dotnet.d.cts +147 -0
- package/dist/plans/dotnet.d.ts +147 -0
- package/dist/plans/dotnet.js +192 -0
- package/package.json +22 -9
- package/src/build-ir.js +0 -220
- package/src/build-plan.js +0 -16
- package/src/build-request.js +0 -46
- package/src/dotnet/build-plan.js +0 -215
- package/src/execute-plan.js +0 -38
- package/src/load-spec.js +0 -9
- package/src/main.js +0 -30
- package/src/parse-input.js +0 -49
- package/src/prepare-output.js +0 -12
- /package/{src/dotnet/template → dist/templates/dotnet}/HostRole.cs.ejs +0 -0
- /package/{src/dotnet/template → dist/templates/dotnet}/Model.cs.ejs +0 -0
- /package/{src/dotnet/template → dist/templates/dotnet}/Network.cs.ejs +0 -0
- /package/{src/dotnet/template → dist/templates/dotnet}/RemoteRole.cs.ejs +0 -0
- /package/{src/dotnet/template → dist/templates/dotnet}/Service.asmdef.ejs +0 -0
- /package/{src/dotnet/template → dist/templates/dotnet}/UserHostRole.cs.ejs +0 -0
- /package/{src/dotnet/template → dist/templates/dotnet}/UserService.asmref.ejs +0 -0
package/dist/main.cjs
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/build-ir.ts
|
|
27
|
+
var import_schema = __toESM(require("@pocketgems/schema"), 1);
|
|
28
|
+
var validateIr = import_schema.default.obj({
|
|
29
|
+
package: import_schema.default.obj({
|
|
30
|
+
project: import_schema.default.str,
|
|
31
|
+
service: import_schema.default.str,
|
|
32
|
+
description: import_schema.default.str.optional(),
|
|
33
|
+
version: import_schema.default.str.optional()
|
|
34
|
+
}),
|
|
35
|
+
networks: import_schema.default.arr(
|
|
36
|
+
import_schema.default.obj({
|
|
37
|
+
name: import_schema.default.str,
|
|
38
|
+
description: import_schema.default.str.optional(),
|
|
39
|
+
version: import_schema.default.str.optional(),
|
|
40
|
+
roles: import_schema.default.arr(
|
|
41
|
+
import_schema.default.obj({
|
|
42
|
+
name: import_schema.default.str,
|
|
43
|
+
description: import_schema.default.str.optional(),
|
|
44
|
+
isHost: import_schema.default.bool,
|
|
45
|
+
endpoints: import_schema.default.arr(
|
|
46
|
+
import_schema.default.obj({
|
|
47
|
+
scheme: import_schema.default.str.enum("ws", "wss"),
|
|
48
|
+
host: import_schema.default.str,
|
|
49
|
+
port: import_schema.default.int.min(0).max(65535),
|
|
50
|
+
path: import_schema.default.str
|
|
51
|
+
})
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
),
|
|
55
|
+
handlers: import_schema.default.arr(
|
|
56
|
+
import_schema.default.obj({
|
|
57
|
+
roleName: import_schema.default.str,
|
|
58
|
+
handlerName: import_schema.default.str,
|
|
59
|
+
description: import_schema.default.str.optional()
|
|
60
|
+
})
|
|
61
|
+
),
|
|
62
|
+
messages: import_schema.default.arr(
|
|
63
|
+
import_schema.default.obj({
|
|
64
|
+
roleName: import_schema.default.str,
|
|
65
|
+
handlerName: import_schema.default.str,
|
|
66
|
+
description: import_schema.default.str.optional()
|
|
67
|
+
})
|
|
68
|
+
),
|
|
69
|
+
models: import_schema.default.arr(
|
|
70
|
+
import_schema.default.obj({
|
|
71
|
+
scopeName: import_schema.default.str,
|
|
72
|
+
modelName: import_schema.default.str,
|
|
73
|
+
type: import_schema.default.str,
|
|
74
|
+
description: import_schema.default.str.optional(),
|
|
75
|
+
properties: import_schema.default.arr(
|
|
76
|
+
import_schema.default.obj({
|
|
77
|
+
type: import_schema.default.str,
|
|
78
|
+
scopeName: import_schema.default.str,
|
|
79
|
+
modelName: import_schema.default.str,
|
|
80
|
+
description: import_schema.default.str.optional(),
|
|
81
|
+
required: import_schema.default.bool.optional(),
|
|
82
|
+
items: import_schema.default.obj({
|
|
83
|
+
type: import_schema.default.str,
|
|
84
|
+
scopeName: import_schema.default.str,
|
|
85
|
+
modelName: import_schema.default.str,
|
|
86
|
+
description: import_schema.default.str.optional()
|
|
87
|
+
}).optional()
|
|
88
|
+
})
|
|
89
|
+
).optional()
|
|
90
|
+
})
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
).desc("An array of network definitions")
|
|
94
|
+
}).compile("IrValidator");
|
|
95
|
+
function buildIrModels(scopeName, modelName, schema) {
|
|
96
|
+
const type = schema.type;
|
|
97
|
+
const model = {
|
|
98
|
+
type,
|
|
99
|
+
scopeName,
|
|
100
|
+
modelName,
|
|
101
|
+
description: schema.description
|
|
102
|
+
};
|
|
103
|
+
switch (type) {
|
|
104
|
+
case "string":
|
|
105
|
+
case "number":
|
|
106
|
+
case "integer":
|
|
107
|
+
case "boolean":
|
|
108
|
+
case "null":
|
|
109
|
+
return [];
|
|
110
|
+
case "array": {
|
|
111
|
+
if (!schema.items) return [];
|
|
112
|
+
return buildIrModels(scopeName, modelName, schema.items);
|
|
113
|
+
}
|
|
114
|
+
case "object": {
|
|
115
|
+
const properties = [];
|
|
116
|
+
model.properties = properties;
|
|
117
|
+
const models = [];
|
|
118
|
+
if (!schema.properties) return [model];
|
|
119
|
+
for (const [subName, subSchema] of Object.entries(schema.properties)) {
|
|
120
|
+
const subModels = buildIrModels(scopeName, subName, subSchema);
|
|
121
|
+
const mainModel = subModels.find((m) => m.modelName === subName) ?? subSchema;
|
|
122
|
+
const property = {
|
|
123
|
+
type: mainModel.type,
|
|
124
|
+
description: mainModel.description,
|
|
125
|
+
scopeName: mainModel.scopeName ?? scopeName,
|
|
126
|
+
modelName: mainModel.modelName ?? subName,
|
|
127
|
+
required: schema.required?.includes(subName)
|
|
128
|
+
};
|
|
129
|
+
const itemsSource = mainModel.properties?.[0]?.items ?? mainModel.items;
|
|
130
|
+
if (itemsSource) {
|
|
131
|
+
property.items = {
|
|
132
|
+
type: itemsSource.type,
|
|
133
|
+
description: itemsSource.description,
|
|
134
|
+
scopeName: itemsSource?.scopeName ?? scopeName,
|
|
135
|
+
modelName: itemsSource?.modelName ?? subName
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
properties.push(property);
|
|
139
|
+
models.push(...subModels);
|
|
140
|
+
}
|
|
141
|
+
return [model, ...models];
|
|
142
|
+
}
|
|
143
|
+
default:
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function buildIr(ctx) {
|
|
148
|
+
const { request, spec } = ctx;
|
|
149
|
+
if (!request) throw new Error("request is required");
|
|
150
|
+
if (!spec) throw new Error("spec is required");
|
|
151
|
+
const { hostRoles } = request;
|
|
152
|
+
const ir = {
|
|
153
|
+
package: {
|
|
154
|
+
project: request.project,
|
|
155
|
+
service: spec.name,
|
|
156
|
+
description: spec.description,
|
|
157
|
+
version: spec.version
|
|
158
|
+
},
|
|
159
|
+
networks: []
|
|
160
|
+
};
|
|
161
|
+
for (const [networkName, networkSpec] of Object.entries(spec.networks)) {
|
|
162
|
+
const hostRoleSpecs = hostRoles.map((hostRole) => networkSpec.roles[hostRole]);
|
|
163
|
+
const otherRoleSpecs = {};
|
|
164
|
+
for (const [roleName, roleSpec] of Object.entries(networkSpec.roles)) {
|
|
165
|
+
if (!hostRoles.includes(roleName)) {
|
|
166
|
+
otherRoleSpecs[roleName] = roleSpec;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const requiredRoles = /* @__PURE__ */ new Set();
|
|
170
|
+
for (const hostRoleSpec of hostRoleSpecs) {
|
|
171
|
+
for (const handlerSpec of Object.values(hostRoleSpec.messages)) {
|
|
172
|
+
if (handlerSpec.from) {
|
|
173
|
+
for (const fromRoleName of handlerSpec.from) {
|
|
174
|
+
requiredRoles.add(fromRoleName);
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
for (const key of Object.keys(otherRoleSpecs)) {
|
|
178
|
+
requiredRoles.add(key);
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const irNetwork = {
|
|
185
|
+
name: networkName,
|
|
186
|
+
description: networkSpec.description,
|
|
187
|
+
version: networkSpec.version,
|
|
188
|
+
roles: [],
|
|
189
|
+
handlers: [],
|
|
190
|
+
messages: [],
|
|
191
|
+
models: []
|
|
192
|
+
};
|
|
193
|
+
for (const hostRoleSpec of hostRoleSpecs) {
|
|
194
|
+
irNetwork.roles.push({
|
|
195
|
+
name: hostRoleSpec.name,
|
|
196
|
+
description: hostRoleSpec.description,
|
|
197
|
+
isHost: true,
|
|
198
|
+
endpoints: hostRoleSpec.endpoints || []
|
|
199
|
+
});
|
|
200
|
+
for (const [handlerName, handlerSpec] of Object.entries(hostRoleSpec.messages)) {
|
|
201
|
+
irNetwork.handlers.push({
|
|
202
|
+
roleName: hostRoleSpec.name,
|
|
203
|
+
handlerName,
|
|
204
|
+
description: handlerSpec.description
|
|
205
|
+
});
|
|
206
|
+
irNetwork.models.push(
|
|
207
|
+
...buildIrModels(hostRoleSpec.name, handlerName, handlerSpec.payload)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
for (const [roleName, roleSpec] of Object.entries(otherRoleSpecs)) {
|
|
212
|
+
if (!requiredRoles.has(roleName)) continue;
|
|
213
|
+
irNetwork.roles.push({
|
|
214
|
+
name: roleName,
|
|
215
|
+
description: roleSpec.description,
|
|
216
|
+
isHost: false,
|
|
217
|
+
endpoints: roleSpec.endpoints || []
|
|
218
|
+
});
|
|
219
|
+
for (const [handlerName, handlerSpec] of Object.entries(roleSpec.messages)) {
|
|
220
|
+
irNetwork.messages.push({
|
|
221
|
+
roleName,
|
|
222
|
+
handlerName,
|
|
223
|
+
description: handlerSpec.description
|
|
224
|
+
});
|
|
225
|
+
irNetwork.models.push(...buildIrModels(roleName, handlerName, handlerSpec.payload));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
ir.networks.push(irNetwork);
|
|
229
|
+
}
|
|
230
|
+
validateIr(ir);
|
|
231
|
+
return {
|
|
232
|
+
...ctx,
|
|
233
|
+
ir
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/build-plan.ts
|
|
238
|
+
var planIndex = {
|
|
239
|
+
csharp: {
|
|
240
|
+
unity: "./plans/dotnet.js"
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
async function dispatchBuildPlan(ctx) {
|
|
244
|
+
const { request } = ctx;
|
|
245
|
+
if (!request) throw new Error("request is required");
|
|
246
|
+
const language = Object.keys(request.target)[0];
|
|
247
|
+
const targetConfig = request.target[language];
|
|
248
|
+
if (!targetConfig) throw new Error(`No target config for language: ${language}`);
|
|
249
|
+
const environment = targetConfig.environment;
|
|
250
|
+
const planPath = planIndex[language]?.[environment];
|
|
251
|
+
if (!planPath) throw new Error(`No plan for ${language}/${environment}`);
|
|
252
|
+
const { default: plan } = await import(planPath);
|
|
253
|
+
return plan(ctx);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/build-request.ts
|
|
257
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
258
|
+
var import_node_process = __toESM(require("process"), 1);
|
|
259
|
+
var import_schema2 = __toESM(require("@pocketgems/schema"), 1);
|
|
260
|
+
var validateBuildRequest = import_schema2.default.obj({
|
|
261
|
+
specPath: import_schema2.default.str,
|
|
262
|
+
outputPath: import_schema2.default.str,
|
|
263
|
+
project: import_schema2.default.str,
|
|
264
|
+
hostRoles: import_schema2.default.arr(import_schema2.default.str),
|
|
265
|
+
target: import_schema2.default.obj({
|
|
266
|
+
csharp: import_schema2.default.obj({
|
|
267
|
+
environment: import_schema2.default.str.enum("unity"),
|
|
268
|
+
frameworks: import_schema2.default.arr(import_schema2.default.str.enum("newtonsoft")).optional()
|
|
269
|
+
}).optional(),
|
|
270
|
+
javascript: import_schema2.default.obj({
|
|
271
|
+
environment: import_schema2.default.str.enum("node", "browser"),
|
|
272
|
+
frameworks: import_schema2.default.arr(import_schema2.default.str.enum("fastify")).optional()
|
|
273
|
+
}).optional()
|
|
274
|
+
}).min(1).max(1).desc("The target platform to generate code for")
|
|
275
|
+
}).compile("BuildRequestValidator");
|
|
276
|
+
function buildRequest(ctx) {
|
|
277
|
+
const { rawInput } = ctx;
|
|
278
|
+
if (!rawInput) throw new Error("rawInput is required");
|
|
279
|
+
console.log("Host roles:", rawInput.hostRole);
|
|
280
|
+
const request = {
|
|
281
|
+
specPath: import_node_path.default.join(import_node_process.default.cwd(), rawInput.spec),
|
|
282
|
+
outputPath: import_node_path.default.join(import_node_process.default.cwd(), rawInput.out),
|
|
283
|
+
project: rawInput.project,
|
|
284
|
+
hostRoles: rawInput.hostRole,
|
|
285
|
+
target: {
|
|
286
|
+
[rawInput.language]: {
|
|
287
|
+
environment: rawInput.environment,
|
|
288
|
+
frameworks: rawInput.frameworks
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
validateBuildRequest(request);
|
|
293
|
+
return {
|
|
294
|
+
...ctx,
|
|
295
|
+
request
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/execute-plan.ts
|
|
300
|
+
var import_node_fs = __toESM(require("fs"), 1);
|
|
301
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
302
|
+
var import_ejs = __toESM(require("ejs"), 1);
|
|
303
|
+
var rendererCache = {};
|
|
304
|
+
function renderTemplate(templatePath, data) {
|
|
305
|
+
if (rendererCache[templatePath]) {
|
|
306
|
+
return rendererCache[templatePath](data);
|
|
307
|
+
}
|
|
308
|
+
const templateContent = import_node_fs.default.readFileSync(templatePath, "utf8");
|
|
309
|
+
const renderer = import_ejs.default.compile(templateContent);
|
|
310
|
+
rendererCache[templatePath] = renderer;
|
|
311
|
+
return renderer(data);
|
|
312
|
+
}
|
|
313
|
+
function executePlan(ctx) {
|
|
314
|
+
const { plan } = ctx;
|
|
315
|
+
if (!plan) throw new Error("plan is required");
|
|
316
|
+
for (const step of plan) {
|
|
317
|
+
switch (step.command) {
|
|
318
|
+
case "copy":
|
|
319
|
+
if (step.input) {
|
|
320
|
+
import_node_fs.default.cpSync(step.input, step.output, { recursive: true });
|
|
321
|
+
}
|
|
322
|
+
break;
|
|
323
|
+
case "render": {
|
|
324
|
+
const { getData, template, output } = step;
|
|
325
|
+
if (!getData || !template) continue;
|
|
326
|
+
const data = getData();
|
|
327
|
+
console.log(data);
|
|
328
|
+
import_node_fs.default.mkdirSync(import_node_path2.default.dirname(output), { recursive: true });
|
|
329
|
+
import_node_fs.default.writeFileSync(output, renderTemplate(template, { ctx: data }));
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return ctx;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/load-spec.ts
|
|
338
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
339
|
+
function loadSpec(ctx) {
|
|
340
|
+
const { request } = ctx;
|
|
341
|
+
if (!request) throw new Error("request is required");
|
|
342
|
+
const { specPath } = request;
|
|
343
|
+
const spec = JSON.parse(import_node_fs2.default.readFileSync(specPath, "utf8"));
|
|
344
|
+
return {
|
|
345
|
+
...ctx,
|
|
346
|
+
spec
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/parse-input.ts
|
|
351
|
+
var import_yargs = __toESM(require("yargs"), 1);
|
|
352
|
+
var import_helpers = require("yargs/helpers");
|
|
353
|
+
function parseInput(ctx) {
|
|
354
|
+
const args = (0, import_yargs.default)((0, import_helpers.hideBin)(ctx.argv)).scriptName("openws-sdkgen").version(false).option("spec", {
|
|
355
|
+
type: "string",
|
|
356
|
+
description: "The path to the OpenWS spec file",
|
|
357
|
+
demandOption: true
|
|
358
|
+
}).option("out", {
|
|
359
|
+
type: "string",
|
|
360
|
+
description: "The path to the output directory",
|
|
361
|
+
demandOption: true
|
|
362
|
+
}).option("project", {
|
|
363
|
+
type: "string",
|
|
364
|
+
description: "The project name",
|
|
365
|
+
demandOption: true
|
|
366
|
+
}).option("hostRole", {
|
|
367
|
+
type: "array",
|
|
368
|
+
string: true,
|
|
369
|
+
description: "The target participant roles that use the generated code",
|
|
370
|
+
demandOption: true
|
|
371
|
+
}).option("language", {
|
|
372
|
+
type: "string",
|
|
373
|
+
description: "The language to generate code for",
|
|
374
|
+
choices: ["csharp", "javascript"],
|
|
375
|
+
default: "csharp"
|
|
376
|
+
}).option("environment", {
|
|
377
|
+
type: "string",
|
|
378
|
+
description: "The environment to generate code for",
|
|
379
|
+
choices: ["unity", "node", "browser"],
|
|
380
|
+
default: "unity"
|
|
381
|
+
}).option("frameworks", {
|
|
382
|
+
type: "array",
|
|
383
|
+
string: true,
|
|
384
|
+
description: "The frameworks to generate code for",
|
|
385
|
+
choices: ["fastify", "newtonsoft"]
|
|
386
|
+
}).strict().help().parseSync();
|
|
387
|
+
return {
|
|
388
|
+
...ctx,
|
|
389
|
+
rawInput: args
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/prepare-output.ts
|
|
394
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
395
|
+
function prepareOutput(ctx) {
|
|
396
|
+
const { request } = ctx;
|
|
397
|
+
if (!request) throw new Error("request is required");
|
|
398
|
+
const { outputPath } = request;
|
|
399
|
+
import_node_fs3.default.rmSync(outputPath, { recursive: true, force: true });
|
|
400
|
+
import_node_fs3.default.mkdirSync(outputPath, { recursive: true });
|
|
401
|
+
return ctx;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/main.ts
|
|
405
|
+
var Pipeline = [
|
|
406
|
+
parseInput,
|
|
407
|
+
buildRequest,
|
|
408
|
+
prepareOutput,
|
|
409
|
+
loadSpec,
|
|
410
|
+
buildIr,
|
|
411
|
+
dispatchBuildPlan,
|
|
412
|
+
executePlan
|
|
413
|
+
];
|
|
414
|
+
async function main() {
|
|
415
|
+
let ctx = { argv: process.argv };
|
|
416
|
+
for (const step of Pipeline) {
|
|
417
|
+
ctx = await step(ctx);
|
|
418
|
+
}
|
|
419
|
+
return ctx;
|
|
420
|
+
}
|
|
421
|
+
main().then((result) => {
|
|
422
|
+
console.log(JSON.stringify(result, null, 2));
|
|
423
|
+
}).catch((err) => {
|
|
424
|
+
console.error(err);
|
|
425
|
+
process.exit(1);
|
|
426
|
+
});
|
package/dist/main.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|