okai 0.0.5 → 0.0.8

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/index.js CHANGED
@@ -2,62 +2,347 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import blessed from 'blessed';
4
4
  import { projectInfo } from './info.js';
5
- import { replaceMyApp } from "./utils.js";
6
- export async function cli(args) {
7
- const text = args.join(' ').trim();
8
- if (text === 'init') {
9
- let info = {};
10
- try {
11
- info = projectInfo(process.cwd());
5
+ import { getGroupName, leftPart, replaceMyApp, trimStart } from "./utils.js";
6
+ import { toAst } from "./ts-ast.js";
7
+ import { toMetadataTypes } from "./cs-ast.js";
8
+ import { CSharpApiGenerator } from "./cs-apis.js";
9
+ import { CSharpMigrationGenerator } from "./cs-migrations.js";
10
+ import { TsdDataModelGenerator } from "./tsd-gen.js";
11
+ import { parseTsdHeader, toTsdHeader } from "./client.js";
12
+ function normalizeSwitches(cmd) { return cmd.replace(/^-+/, '/'); }
13
+ function parseArgs(...args) {
14
+ const ret = {
15
+ type: "help",
16
+ baseUrl: process.env.OKAI_URL || "https://okai.servicestack.com",
17
+ script: path.basename(process.argv[1]) || "okai",
18
+ };
19
+ for (let i = 0; i < args.length; i++) {
20
+ const arg = args[i];
21
+ const opt = normalizeSwitches(arg);
22
+ if (opt.startsWith('/')) {
23
+ switch (opt) {
24
+ case "/?":
25
+ case "/h":
26
+ case "/help":
27
+ ret.type = "help";
28
+ break;
29
+ case "/v":
30
+ case "/verbose":
31
+ ret.verbose = true;
32
+ break;
33
+ case "/D":
34
+ ret.verbose = ret.debug = true;
35
+ break;
36
+ case "/w":
37
+ case "/watch":
38
+ ret.watch = true;
39
+ break;
40
+ case "/m":
41
+ case "/models":
42
+ ret.models = args[++i];
43
+ break;
44
+ case "/l":
45
+ case "/license":
46
+ ret.license = args[++i];
47
+ break;
48
+ default:
49
+ ret.unknown = ret.unknown || [];
50
+ ret.unknown.push(arg);
51
+ break;
52
+ }
12
53
  }
13
- catch (err) {
14
- info = {
15
- projectName: "",
16
- sln: "",
17
- slnDir: "",
18
- hostDir: "",
19
- migrationsDir: "",
20
- serviceModelDir: "",
21
- serviceInterfaceDir: "",
22
- };
54
+ else if (ret.type === "help" && ["help", "info", "init", "ls", "rm"].includes(arg)) {
55
+ if (arg == "help")
56
+ ret.type = "help";
57
+ else if (arg == "info")
58
+ ret.type = "info";
59
+ else if (arg == "init")
60
+ ret.type = "init";
61
+ else if (arg == "rm")
62
+ ret.type = "remove";
63
+ else if (arg == "ls") {
64
+ ret.type = "list";
65
+ ret.list = args[++i];
66
+ }
67
+ }
68
+ else if (arg.endsWith('.d.ts')) {
69
+ if (ret.type == "help")
70
+ ret.type = "update";
71
+ ret.tsdFile = arg;
72
+ }
73
+ else {
74
+ ret.type = "prompt";
75
+ if (ret.prompt) {
76
+ ret.prompt += ' ';
77
+ }
78
+ ret.prompt = (ret.prompt ?? '') + arg;
23
79
  }
80
+ }
81
+ if (ret.type === "prompt") {
82
+ if (!ret.models && process.env.OKAI_MODELS) {
83
+ ret.models = process.env.OKAI_MODELS;
84
+ }
85
+ if (ret.models) {
86
+ if (!ret.license && process.env.SERVICESTACK_CERTIFICATE) {
87
+ ret.license = process.env.SERVICESTACK_CERTIFICATE;
88
+ }
89
+ if (!ret.license && process.env.SERVICESTACK_LICENSE) {
90
+ ret.license = process.env.SERVICESTACK_LICENSE;
91
+ }
92
+ }
93
+ }
94
+ return ret;
95
+ }
96
+ export async function cli(cmdArgs) {
97
+ const command = parseArgs(...cmdArgs);
98
+ const script = command.script;
99
+ if (command.verbose) {
100
+ console.log(`Command: ${JSON.stringify(command, undefined, 2)}`);
101
+ }
102
+ if (command.debug) {
103
+ console.log(`Environment:`);
104
+ Object.keys(process.env).forEach(k => {
105
+ console.log(`${k}: ${process.env[k]}`);
106
+ });
107
+ process.exit(0);
108
+ return;
109
+ }
110
+ if (command.type === "init") {
111
+ let info = projectInfo(process.cwd()) ?? {
112
+ projectName: "<MyApp>",
113
+ slnDir: "/path/to/.sln/folder",
114
+ hostDir: "/path/to/MyApp",
115
+ migrationsDir: "/path/to/MyApp/Migrations",
116
+ serviceModelDir: "/path/to/MyApp.ServiceModel",
117
+ serviceInterfaceDir: "/path/to/MyApp.ServiceInterfaces",
118
+ };
24
119
  fs.writeFileSync('okai.json', JSON.stringify(info, undefined, 2));
25
120
  process.exit(0);
121
+ return;
26
122
  }
27
- if (!text || text === '?' || text === 'help') {
123
+ if (command.type === "help" || command.unknown?.length) {
124
+ const exitCode = command.unknown?.length ? 1 : 0;
125
+ if (command.unknown?.length) {
126
+ console.log(`Unknown Command: ${command.script} ${command.unknown.join(' ')}\n`);
127
+ }
28
128
  console.log(`Usage:
29
- okai init - Initialize okai.json with project info to override default paths
30
- okai <prompt> - Generate new APIs and Tables for the specified prompt`);
129
+ ${script} <prompt> Generate new TypeScript Data Models, C# APIs and Migrations from prompt
130
+ -m, -models <model,> Specify up to 5 LLM models to generate .d.ts Data Models
131
+ -l, -license <LC-xxx> Specify valid license certificate or key to use premium models
132
+
133
+ ${script} <models>.d.ts Regenerate C# *.cs files for Data Models defined in the TypeScript .d.ts file
134
+ -w, -watch Watch for changes to <models>.d.ts and regenerate *.cs on save
135
+
136
+ ${script} rm <models>.d.ts Remove <models>.d.ts and its generated *.cs files
137
+ ${script} ls models Display list of available premium LLM models
138
+ ${script} init Initialize okai.json with project info to override default paths
139
+ ${script} info Display current project info
140
+
141
+ Options:
142
+ -v, -verbose Display verbose logging
143
+ --ignore-ssl-errors Ignore SSL Errors`);
144
+ process.exit(exitCode);
145
+ return;
146
+ }
147
+ const info = command.info = projectInfo(process.cwd());
148
+ if (!info) {
149
+ if (!info) {
150
+ console.log(`No .sln file found`);
151
+ console.log(`okai needs to be run within a ServiceStack App that contains a ServiceModel project`);
152
+ console.log(`To use with an external or custom project, create an okai.json config file with:`);
153
+ console.log(`$ ${script} init`);
154
+ process.exit(1);
155
+ }
156
+ }
157
+ if (command.type === "info") {
158
+ try {
159
+ console.log(JSON.stringify(command.info, undefined, 2));
160
+ }
161
+ catch (err) {
162
+ console.error(err.message ?? `${err}`);
163
+ }
31
164
  process.exit(0);
32
165
  return;
33
166
  }
34
- const baseUrl = process.env.OKAI_URL || 'https://okai.servicestack.com';
35
- try {
36
- const info = projectInfo(process.cwd());
37
- if (!info.serviceModelDir)
38
- throw new Error("Could not find ServiceModel directory");
39
- console.log(`Generating new APIs and Tables for: ${text}...`);
40
- const gist = await fetchGistFiles(baseUrl, text);
41
- const projectGist = convertToProjectGist(info, gist);
42
- const ctx = await createGistPreview(text, projectGist);
43
- ctx.screen.key('a', () => applyGist(ctx, info, projectGist, { accept: true }));
44
- ctx.screen.key('d', () => applyGist(ctx, info, projectGist, { discard: true }));
45
- ctx.screen.key('S-a', () => applyGist(ctx, info, projectGist, { acceptAll: true }));
46
- ctx.screen.render();
47
- }
48
- catch (err) {
49
- console.error(err);
167
+ if (command.ignoreSsl) {
168
+ process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
169
+ }
170
+ if (command.type === "list") {
171
+ if (command.list == "models") {
172
+ const url = new URL('/models/list', command.baseUrl);
173
+ if (command.verbose)
174
+ console.log(`GET: ${url}`);
175
+ const res = await fetch(url);
176
+ if (!res.ok) {
177
+ console.log(`Failed to fetch models: ${res.statusText}`);
178
+ process.exit(1);
179
+ }
180
+ const models = await res.text();
181
+ console.log(models);
182
+ process.exit(0);
183
+ }
184
+ }
185
+ function assertTsdPath(tsdFile) {
186
+ const tryPaths = [
187
+ path.join(process.cwd(), tsdFile),
188
+ ];
189
+ if (info?.serviceModelDir) {
190
+ tryPaths.push(path.join(info.serviceModelDir, tsdFile));
191
+ }
192
+ const tsdPath = tryPaths.find(fs.existsSync);
193
+ if (!tsdPath) {
194
+ console.log(`Could not find: ${command.tsdFile}, tried:\n${tryPaths.join('\n')}`);
195
+ process.exit(1);
196
+ }
197
+ return tsdPath;
198
+ }
199
+ function resolveMigrationFile(migrationPath) {
200
+ return migrationPath.startsWith('~/')
201
+ ? path.join(info.slnDir, trimStart(migrationPath, '~/'))
202
+ : path.join(process.cwd(), migrationPath);
203
+ }
204
+ function resolveApiFile(apiPath) {
205
+ return apiPath.startsWith('~/')
206
+ ? path.join(info.slnDir, trimStart(apiPath, '~/'))
207
+ : path.join(process.cwd(), apiPath);
208
+ }
209
+ if (command.type === "update") {
210
+ let tsdPath = assertTsdPath(command.tsdFile);
211
+ if (command.verbose)
212
+ console.log(`Updating: ${tsdPath}...`);
213
+ let tsdContent = fs.readFileSync(tsdPath, 'utf-8');
214
+ const header = parseTsdHeader(tsdContent);
215
+ if (command.verbose)
216
+ console.log(JSON.stringify(header, undefined, 2));
217
+ function regenerate(header, tsdContent, logPrefix = '') {
218
+ const tsdAst = toAst(tsdContent);
219
+ const tsdGenerator = new TsdDataModelGenerator();
220
+ tsdContent = tsdGenerator.generate(tsdAst);
221
+ const csAst = toMetadataTypes(tsdAst);
222
+ // const groupName = path.basename(command.tsdFile, '.d.ts')
223
+ // console.log('groupName', groupName)
224
+ const genApis = new CSharpApiGenerator();
225
+ const csApiFiles = genApis.generate(csAst);
226
+ const apiContent = replaceMyApp(csApiFiles[Object.keys(csApiFiles)[0]], info.projectName);
227
+ const apiPath = resolveApiFile(header.api);
228
+ console.log(`${logPrefix}${apiPath}`);
229
+ fs.writeFileSync(apiPath, apiContent, { encoding: 'utf-8' });
230
+ if (header?.migration) {
231
+ const migrationCls = leftPart(path.basename(header.migration), '.');
232
+ const getMigrations = new CSharpMigrationGenerator();
233
+ const csMigrationFiles = getMigrations.generate(csAst);
234
+ const migrationContent = replaceMyApp(csMigrationFiles[Object.keys(csMigrationFiles)[0]].replaceAll('Migration1000', migrationCls), info.projectName);
235
+ const migrationPath = resolveApiFile(header.migration);
236
+ console.log(`${logPrefix}${migrationPath}`);
237
+ fs.writeFileSync(migrationPath, migrationContent, { encoding: 'utf-8' });
238
+ }
239
+ console.log(`${logPrefix}${tsdPath}`);
240
+ const newTsdContent = toTsdHeader(header) + '\n\n' + tsdContent;
241
+ fs.writeFileSync(tsdPath, newTsdContent, { encoding: 'utf-8' });
242
+ return newTsdContent;
243
+ }
244
+ if (command.watch) {
245
+ let lastTsdContent = tsdContent;
246
+ console.log(`watching ${tsdPath} ...`);
247
+ let i = 0;
248
+ fs.watchFile(tsdPath, { interval: 100 }, (curr, prev) => {
249
+ let tsdContent = fs.readFileSync(tsdPath, 'utf-8');
250
+ if (tsdContent == lastTsdContent) {
251
+ if (command.verbose)
252
+ console.log(`No change detected`);
253
+ return;
254
+ }
255
+ console.log(`\n${++i}. ${leftPart(new Date().toTimeString(), ' ')} regenerating files:`);
256
+ lastTsdContent = regenerate(header, tsdContent);
257
+ });
258
+ return;
259
+ }
260
+ else {
261
+ regenerate(header, tsdContent, 'saved: ');
262
+ console.log(`\nLast migration can be rerun with 'npm run rerun:last' or:`);
263
+ console.log(`$ dotnet run --AppTasks=migrate.rerun:last`);
264
+ process.exit(0);
265
+ }
266
+ }
267
+ if (command.type === 'remove') {
268
+ let tsdPath = assertTsdPath(command.tsdFile);
269
+ if (command.verbose)
270
+ console.log(`Removing: ${tsdPath}...`);
271
+ const tsdContent = fs.readFileSync(tsdPath, 'utf-8');
272
+ const header = parseTsdHeader(tsdContent);
273
+ if (command.verbose)
274
+ console.log(JSON.stringify(header, undefined, 2));
275
+ if (header?.migration) {
276
+ const migrationPath = resolveMigrationFile(header.migration);
277
+ if (fs.existsSync(migrationPath)) {
278
+ fs.unlinkSync(migrationPath);
279
+ console.log(`Removed: ${migrationPath}`);
280
+ }
281
+ else {
282
+ console.log(`Migration .cs file not found: ${migrationPath}`);
283
+ }
284
+ }
285
+ if (header?.api) {
286
+ const apiPath = resolveApiFile(header.api);
287
+ if (fs.existsSync(apiPath)) {
288
+ fs.unlinkSync(apiPath);
289
+ console.log(`Removed: ${apiPath}`);
290
+ }
291
+ else {
292
+ console.log(`APIs .cs file not found: ${apiPath}`);
293
+ }
294
+ }
295
+ fs.unlinkSync(tsdPath);
296
+ console.log(`Removed: ${tsdPath}`);
297
+ process.exit(0);
298
+ }
299
+ if (command.type === 'prompt') {
300
+ try {
301
+ if (!info.serviceModelDir)
302
+ throw new Error("Could not find ServiceModel directory");
303
+ console.log(`Generating new APIs and Tables for: ${command.prompt}...`);
304
+ const gist = await fetchGistFiles(command);
305
+ // const projectGist = convertToProjectGist(info, gist)
306
+ const ctx = await createGistPreview(command.prompt, gist);
307
+ ctx.screen.key('a', () => chooseFile(ctx, info, gist));
308
+ // ctx.screen.key('a', () => applyCSharpGist(ctx, info, gist, { accept: true }))
309
+ // ctx.screen.key('d', () => applyCSharpGist(ctx, info, gist, { discard: true }))
310
+ // ctx.screen.key('S-a', () => applyCSharpGist(ctx, info, gist, { acceptAll: true }))
311
+ ctx.screen.render();
312
+ }
313
+ catch (err) {
314
+ console.error(err);
315
+ }
316
+ }
317
+ else {
318
+ console.log(`Unknown command: ${command.type}`);
319
+ process.exit(1);
50
320
  }
51
321
  }
52
- async function fetchGistFiles(baseUrl, text) {
53
- const url = new URL('/gist', baseUrl);
322
+ async function fetchGistFiles(command) {
323
+ const url = new URL('/models/gist', command.baseUrl);
54
324
  if (process.env.OKAI_CACHED) {
55
325
  url.searchParams.append('cached', `1`);
56
326
  }
57
- url.searchParams.append('text', text);
327
+ url.searchParams.append('prompt', command.prompt);
328
+ if (command.models) {
329
+ url.searchParams.append('models', command.models);
330
+ if (command.license) {
331
+ url.searchParams.append('license', command.license);
332
+ }
333
+ }
334
+ if (command.verbose)
335
+ console.log(`GET: ${url}`);
58
336
  const res = await fetch(url);
59
337
  if (!res.ok) {
60
- throw new Error(`Failed to generate files: ${res.statusText}`);
338
+ try {
339
+ const errorResponse = await res.json();
340
+ console.error(errorResponse?.responseStatus?.message ?? errorResponse.message ?? errorResponse);
341
+ }
342
+ catch (err) {
343
+ console.log(`Failed to generate data models: ${res.statusText}`);
344
+ }
345
+ process.exit(1);
61
346
  }
62
347
  const gist = await res.json();
63
348
  const files = gist.files;
@@ -66,33 +351,44 @@ async function fetchGistFiles(baseUrl, text) {
66
351
  }
67
352
  return gist;
68
353
  }
354
+ // When writing to disk, replace MyApp with the project name
69
355
  function convertToProjectGist(info, gist) {
70
356
  const to = Object.assign({}, gist, { files: {} });
71
357
  const cwd = process.cwd();
72
- for (const [fileName, file] of Object.entries(gist.files)) {
73
- if (fileName.startsWith('MyApp.ServiceModel/') && info.serviceModelDir) {
74
- const fullPath = path.join(info.serviceModelDir, file.filename.substring('MyApp.ServiceModel/'.length));
358
+ for (const [displayName, file] of Object.entries(gist.files)) {
359
+ const writeFileName = file.filename;
360
+ const type = `text/csharp`;
361
+ const content = replaceMyApp(file.content, info.projectName);
362
+ const size = content.length;
363
+ if (writeFileName.startsWith('MyApp.ServiceModel/') && info.serviceModelDir) {
364
+ const fullPath = path.join(info.serviceModelDir, writeFileName.substring('MyApp.ServiceModel/'.length));
75
365
  const relativePath = path.relative(cwd, fullPath);
76
366
  to.files[relativePath] = {
77
367
  filename: path.basename(fullPath),
78
- content: replaceMyApp(gist.files[fileName].content, info.projectName)
368
+ content,
369
+ type,
370
+ size,
79
371
  };
80
372
  }
81
- else if (fileName.startsWith('MyApp/Migrations/') && info.migrationsDir) {
82
- const fullPath = path.join(info.migrationsDir, file.filename.substring('MyApp.Migrations/'.length));
373
+ else if (writeFileName.startsWith('MyApp/Migrations/') && info.migrationsDir) {
374
+ const fullPath = path.join(info.migrationsDir, writeFileName.substring('MyApp.Migrations/'.length));
83
375
  const relativePath = path.relative(cwd, fullPath);
84
376
  to.files[relativePath] = Object.assign({}, file, {
85
377
  filename: path.basename(fullPath),
86
- content: replaceMyApp(gist.files[fileName].content, info.projectName)
378
+ content,
379
+ type,
380
+ size,
87
381
  });
88
382
  }
89
383
  else {
90
- const fullPath = path.join(info.slnDir, file.filename);
384
+ const fullPath = path.join(info.slnDir, writeFileName);
91
385
  const relativePath = path.relative(cwd, fullPath);
92
386
  const toFilename = replaceMyApp(relativePath, info.projectName);
93
387
  to.files[relativePath] = Object.assign({}, file, {
94
388
  filename: path.basename(toFilename),
95
- content: replaceMyApp(file.content, info.projectName)
389
+ content,
390
+ type,
391
+ size,
96
392
  });
97
393
  }
98
394
  }
@@ -159,7 +455,7 @@ async function createGistPreview(title, gist) {
159
455
  left: 0,
160
456
  width: '100%',
161
457
  height: 1,
162
- content: 'Press (a) accept (d) discard (A) Accept All (q) quit',
458
+ content: 'Press (a) accept (q) quit',
163
459
  style: {
164
460
  fg: 'white',
165
461
  bg: 'blue'
@@ -190,6 +486,58 @@ async function createGistPreview(title, gist) {
190
486
  // Render screen
191
487
  return { screen, titleBar, fileList, preview, statusBar, result };
192
488
  }
489
+ function chooseFile(ctx, info, gist) {
490
+ const { screen, titleBar, fileList, preview, statusBar, result } = ctx;
491
+ const file = gist.files[result.selectedFile];
492
+ screen.destroy();
493
+ const tsd = file.content;
494
+ const tsdAst = toAst(tsd);
495
+ const csAst = toMetadataTypes(tsdAst);
496
+ const groupName = getGroupName(csAst);
497
+ const genApis = new CSharpApiGenerator();
498
+ const csApiFiles = genApis.generate(csAst);
499
+ const getMigrations = new CSharpMigrationGenerator();
500
+ const csMigrationFiles = getMigrations.generate(csAst);
501
+ const relativeServiceModelDir = trimStart(info.serviceModelDir.substring(info.slnDir.length), '~/');
502
+ const relativeMigrationDir = trimStart(info.migrationsDir.substring(info.slnDir.length), '~/');
503
+ const apiFileName = `${groupName}.cs`;
504
+ const apiContent = replaceMyApp(csApiFiles[Object.keys(csApiFiles)[0]], info.projectName);
505
+ const migrationPath = resolveMigrationFile(path.join(info.migrationsDir, `Migration1000.cs`));
506
+ const migrationFileName = path.basename(migrationPath);
507
+ const migrationCls = leftPart(migrationFileName, '.');
508
+ const migrationContent = replaceMyApp(csMigrationFiles[Object.keys(csMigrationFiles)[0]].replaceAll('Migration1000', migrationCls), info.projectName);
509
+ const sb = [];
510
+ sb.push(`/*prompt: ${titleBar.content.replaceAll('/*', '').replaceAll('*/', '')}`);
511
+ sb.push(`api: ~/${path.join(relativeServiceModelDir, apiFileName)}`);
512
+ sb.push(`migration: ~/${path.join(relativeMigrationDir, migrationFileName)}`);
513
+ sb.push(`*/`);
514
+ sb.push('');
515
+ sb.push(tsd);
516
+ const tsdContent = sb.join('\n');
517
+ const tsdFileName = `${groupName}.d.ts`;
518
+ const fullTsdPath = path.join(info.slnDir, relativeServiceModelDir, tsdFileName);
519
+ const fullApiPath = path.join(info.slnDir, relativeServiceModelDir, apiFileName);
520
+ const fullMigrationPath = path.join(info.slnDir, relativeMigrationDir, migrationFileName);
521
+ if (!fs.existsSync(path.dirname(fullTsdPath))) {
522
+ console.log(`Directory does not exist: ${path.dirname(fullTsdPath)}`);
523
+ process.exit(0);
524
+ }
525
+ console.log(`\nSelected '${result.selectedFile}' data models`);
526
+ fs.writeFileSync(fullTsdPath, tsdContent, { encoding: 'utf-8' });
527
+ console.log(`\nSaved: ${fullTsdPath}`);
528
+ if (fs.existsSync(path.dirname(fullApiPath))) {
529
+ fs.writeFileSync(fullApiPath, apiContent, { encoding: 'utf-8' });
530
+ console.log(`Saved: ${fullApiPath}`);
531
+ }
532
+ if (fs.existsSync(path.dirname(fullMigrationPath))) {
533
+ fs.writeFileSync(fullMigrationPath, migrationContent, { encoding: 'utf-8' });
534
+ console.log(`Saved: ${fullMigrationPath}`);
535
+ }
536
+ const script = path.basename(process.argv[1]);
537
+ console.log(`\nTo regenerate classes, update '${tsdFileName}' then run:`);
538
+ console.log(`$ ${script} ${tsdFileName}`);
539
+ process.exit(0);
540
+ }
193
541
  function writeFile(info, filename, content) {
194
542
  let fullPath = path.join(process.cwd(), filename);
195
543
  const dir = path.dirname(fullPath);
@@ -217,6 +565,25 @@ function writeFile(info, filename, content) {
217
565
  }
218
566
  fs.writeFileSync(fullPath, content);
219
567
  }
568
+ function resolveMigrationFile(fullPath) {
569
+ const dir = path.dirname(fullPath);
570
+ const filename = path.basename(fullPath);
571
+ const ext = path.extname(filename);
572
+ const baseName = path.basename(filename, ext);
573
+ // filename: Migration1000.cs, baseName: Migration1000, ext: .cs
574
+ // console.log(`File already exists: ${fullPath}`, { filename, baseName, ext })
575
+ const numberedFile = baseName.match(/(\d+)$/);
576
+ if (numberedFile) {
577
+ let nextNumber = parseInt(numberedFile[1]);
578
+ while (fs.existsSync(fullPath)) {
579
+ if (numberedFile) {
580
+ nextNumber += 1;
581
+ fullPath = path.join(dir, `${baseName.replace(/\d+$/, '')}${nextNumber}${ext}`);
582
+ }
583
+ }
584
+ }
585
+ return fullPath;
586
+ }
220
587
  function exit(screen, info, gist) {
221
588
  screen.destroy();
222
589
  if (info.migrationsDir) {
@@ -224,7 +591,7 @@ function exit(screen, info, gist) {
224
591
  }
225
592
  process.exit(0);
226
593
  }
227
- function applyGist(ctx, info, gist, { accept = false, acceptAll = false, discard = false }) {
594
+ function applyCSharpGist(ctx, info, gist, { accept = false, acceptAll = false, discard = false }) {
228
595
  const { screen, titleBar, fileList, preview, statusBar, result } = ctx;
229
596
  function removeSelected() {
230
597
  delete gist.files[result.selectedFile];
package/dist/info.js CHANGED
@@ -19,7 +19,7 @@ export function projectInfo(cwd) {
19
19
  if (!sln) {
20
20
  if (config)
21
21
  return config;
22
- throw new Error("No .sln file found");
22
+ return null;
23
23
  }
24
24
  const projectName = sln.substring(0, sln.length - 4);
25
25
  function getDir(slnDir, match) {