1- import { relative , resolve , sep } from "path" ;
2- import { pathExists , readFile } from "fs-extra" ;
3- import { load as loadYaml } from "js-yaml" ;
1+ import { join , relative , resolve , sep } from "path" ;
2+ import { outputFile , pathExists , readFile } from "fs-extra" ;
3+ import { dump as dumpYaml , load as loadYaml } from "js-yaml" ;
44import { minimatch } from "minimatch" ;
55import { CancellationToken , window } from "vscode" ;
66import { CodeQLCliServer } from "../cli" ;
7- import { getOnDiskWorkspaceFolders , showAndLogErrorMessage } from "../helpers" ;
7+ import {
8+ getOnDiskWorkspaceFolders ,
9+ getOnDiskWorkspaceFoldersObjects ,
10+ showAndLogErrorMessage ,
11+ } from "../helpers" ;
812import { ProgressCallback } from "../progress" ;
913import { DatabaseItem } from "../local-databases" ;
1014import { getQlPackPath , QLPACK_FILENAMES } from "../pure/ql" ;
1115
1216const maxStep = 3 ;
1317
18+ const packNamePartRegex = / [ a - z 0 - 9 ] (?: [ a - z 0 - 9 - ] * [ a - z 0 - 9 ] ) ? / ;
19+ const packNameRegex = new RegExp (
20+ `^(?:(?<scope>${ packNamePartRegex . source } )/)?(?<name>${ packNamePartRegex . source } )$` ,
21+ ) ;
22+ const packNameLength = 128 ;
23+
1424export async function pickExtensionPackModelFile (
1525 cliServer : Pick < CodeQLCliServer , "resolveQlpacks" | "resolveExtensions" > ,
1626 databaseItem : Pick < DatabaseItem , "name" > ,
1727 progress : ProgressCallback ,
1828 token : CancellationToken ,
1929) : Promise < string | undefined > {
20- const extensionPackPath = await pickExtensionPack ( cliServer , progress , token ) ;
30+ const extensionPackPath = await pickExtensionPack (
31+ cliServer ,
32+ databaseItem ,
33+ progress ,
34+ token ,
35+ ) ;
2136 if ( ! extensionPackPath ) {
2237 return ;
2338 }
@@ -38,6 +53,7 @@ export async function pickExtensionPackModelFile(
3853
3954async function pickExtensionPack (
4055 cliServer : Pick < CodeQLCliServer , "resolveQlpacks" > ,
56+ databaseItem : Pick < DatabaseItem , "name" > ,
4157 progress : ProgressCallback ,
4258 token : CancellationToken ,
4359) : Promise < string | undefined > {
@@ -50,10 +66,20 @@ async function pickExtensionPack(
5066 // Get all existing extension packs in the workspace
5167 const additionalPacks = getOnDiskWorkspaceFolders ( ) ;
5268 const extensionPacks = await cliServer . resolveQlpacks ( additionalPacks , true ) ;
53- const options = Object . keys ( extensionPacks ) . map ( ( pack ) => ( {
54- label : pack ,
55- extensionPack : pack ,
56- } ) ) ;
69+
70+ if ( Object . keys ( extensionPacks ) . length === 0 ) {
71+ return pickNewExtensionPack ( databaseItem , token ) ;
72+ }
73+
74+ const options : Array < { label : string ; extensionPack : string | null } > =
75+ Object . keys ( extensionPacks ) . map ( ( pack ) => ( {
76+ label : pack ,
77+ extensionPack : pack ,
78+ } ) ) ;
79+ options . push ( {
80+ label : "Create new extension pack" ,
81+ extensionPack : null ,
82+ } ) ;
5783
5884 progress ( {
5985 message : "Choosing extension pack..." ,
@@ -72,6 +98,10 @@ async function pickExtensionPack(
7298 return undefined ;
7399 }
74100
101+ if ( ! extensionPackOption . extensionPack ) {
102+ return pickNewExtensionPack ( databaseItem , token ) ;
103+ }
104+
75105 const extensionPackPaths = extensionPacks [ extensionPackOption . extensionPack ] ;
76106 if ( extensionPackPaths . length !== 1 ) {
77107 void showAndLogErrorMessage (
@@ -153,6 +183,89 @@ async function pickModelFile(
153183 return pickNewModelFile ( databaseItem , extensionPackPath , token ) ;
154184}
155185
186+ async function pickNewExtensionPack (
187+ databaseItem : Pick < DatabaseItem , "name" > ,
188+ token : CancellationToken ,
189+ ) : Promise < string | undefined > {
190+ const workspaceFolders = getOnDiskWorkspaceFoldersObjects ( ) ;
191+ const workspaceFolderOptions = workspaceFolders . map ( ( folder ) => ( {
192+ label : folder . name ,
193+ detail : folder . uri . fsPath ,
194+ path : folder . uri . fsPath ,
195+ } ) ) ;
196+
197+ // We're not using window.showWorkspaceFolderPick because that also includes the database source folders while
198+ // we only want to include on-disk workspace folders.
199+ const workspaceFolder = await window . showQuickPick ( workspaceFolderOptions , {
200+ title : "Select workspace folder to create extension pack in" ,
201+ } ) ;
202+ if ( ! workspaceFolder ) {
203+ return undefined ;
204+ }
205+
206+ const packName = await window . showInputBox (
207+ {
208+ title : "Create new extension pack" ,
209+ prompt : "Enter name of extension pack" ,
210+ placeHolder : `e.g. ${ databaseItem . name } -extensions` ,
211+ validateInput : async ( value : string ) : Promise < string | undefined > => {
212+ if ( ! value ) {
213+ return "Pack name must not be empty" ;
214+ }
215+
216+ if ( value . length > packNameLength ) {
217+ return `Pack name must be no longer than ${ packNameLength } characters` ;
218+ }
219+
220+ const matches = packNameRegex . exec ( value ) ;
221+ if ( ! matches ?. groups ) {
222+ return "Invalid package name: a pack name must contain only lowercase ASCII letters, ASCII digits, and hyphens" ;
223+ }
224+
225+ const packPath = join ( workspaceFolder . path , matches . groups . name ) ;
226+ if ( await pathExists ( packPath ) ) {
227+ return `A pack already exists at ${ packPath } ` ;
228+ }
229+
230+ return undefined ;
231+ } ,
232+ } ,
233+ token ,
234+ ) ;
235+ if ( ! packName ) {
236+ return undefined ;
237+ }
238+
239+ const matches = packNameRegex . exec ( packName ) ;
240+ if ( ! matches ?. groups ) {
241+ return ;
242+ }
243+
244+ const name = matches . groups . name ;
245+ const packPath = join ( workspaceFolder . path , name ) ;
246+
247+ if ( await pathExists ( packPath ) ) {
248+ return undefined ;
249+ }
250+
251+ const packYamlPath = join ( packPath , "codeql-pack.yml" ) ;
252+
253+ await outputFile (
254+ packYamlPath ,
255+ dumpYaml ( {
256+ name,
257+ version : "0.0.0" ,
258+ library : true ,
259+ extensionTargets : {
260+ "codeql/java-all" : "*" ,
261+ } ,
262+ dataExtensions : [ "models/**/*.yml" ] ,
263+ } ) ,
264+ ) ;
265+
266+ return packPath ;
267+ }
268+
156269async function pickNewModelFile (
157270 databaseItem : Pick < DatabaseItem , "name" > ,
158271 extensionPackPath : string ,
0 commit comments