Skip to content

Commit b8440b6

Browse files
committed
fix: generate --description cwd
1 parent 648f039 commit b8440b6

File tree

4 files changed

+72
-32
lines changed

4 files changed

+72
-32
lines changed

src/app-definition-generator.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import fs from 'node:fs'
33
import path from 'node:path'
44
import _s from 'underscore.string'
55
import { dump as yamlDump } from 'js-yaml'
6-
import { getEntityNameStrings, paramCase } from './string-utils.js'
6+
import { getEntityNameStrings, kebabCase } from './string-utils.js'
77

88
export interface App {
99
entities: AppEntity[]
@@ -42,8 +42,12 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
4242

4343
const OUTPUT_DIR = process.cwd()
4444

45-
export function getAppOutputDir({ appName }: { appName: string }) {
46-
return path.join(OUTPUT_DIR, paramCase(appName))
45+
export function getAppNameKebabCase({ appName }: { appName: string }) {
46+
return kebabCase(appName)
47+
}
48+
49+
export function getAppOutputDir({ appName, absolute = true }: { appName: string; absolute?: boolean }) {
50+
return absolute ? path.join(OUTPUT_DIR, getAppNameKebabCase({ appName })) : getAppNameKebabCase({ appName })
4751
}
4852

4953
function getCodeGenieDir({ appName }: { appName: string }) {
@@ -103,7 +107,7 @@ function writeAppYamlToFileSystem({ app, appName, appDescription }: { app: App;
103107
const definitions: any = {}
104108
for (const entity of app.entities) {
105109
definitions[_s.camelize(_s.decapitalize(entity.name))] = {
106-
$ref: `./entities/${paramCase(entity.name)}.yml`,
110+
$ref: `./entities/${kebabCase(entity.name)}.yml`,
107111
}
108112
}
109113

@@ -126,7 +130,7 @@ function writeAppYamlToFileSystem({ app, appName, appDescription }: { app: App;
126130

127131
export function getJsonSchemasFromEntities({ app }: { app: App }) {
128132
return app.entities.map((entity) => {
129-
const paramCasedEntityName = paramCase(entity.name)
133+
const paramCasedEntityName = kebabCase(entity.name)
130134
const fileName = `${paramCasedEntityName}.yml`
131135
const codeGenieEntityJsonSchema = convertToCodeGenieEntityJsonSchema({
132136
app,
@@ -267,7 +271,7 @@ function getHasManySettings({ app, entity }: { app: App; entity: AppEntity }) {
267271
for (const appEntity of app.entities) {
268272
if (appEntity.belongsTo === entity.name) {
269273
hasMany[appEntity.name] = {
270-
$ref: `./${paramCase(appEntity.name)}.yml`,
274+
$ref: `./${kebabCase(appEntity.name)}.yml`,
271275
}
272276
}
273277
}

src/commands/generate.ts

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { cosmiconfig } from 'cosmiconfig'
66
import createDebug from 'debug'
77
// import codeGenieSampleOpenAiOutputJson from '../sample-api-output.js'
88
import { App, convertOpenAiOutputToCodeGenieInput, getAppOutputDir } from '../app-definition-generator.js'
9-
import copyAwsProfile from '../copyAwsProfile.js'
9+
import copyAwsProfile, { awsCredentialsFileExists } from '../copyAwsProfile.js'
1010
import sleep from '../sleep.js'
1111
import { execSync } from 'node:child_process'
1212
import { Readable } from 'node:stream'
@@ -86,7 +86,7 @@ generating app...
8686
}),
8787
}
8888

89-
async run(): Promise<void | { description?: string; deploy: boolean; awsProfileToCopy: string }> {
89+
async run(): Promise<{ description?: string; deploy: boolean; awsProfileToCopy: string; appDir: string }> {
9090
const { flags } = await this.parse(Generate)
9191
const { description, deploy, awsProfileToCopy, noCopyAwsProfile } = flags
9292

@@ -97,37 +97,62 @@ generating app...
9797
})
9898
}
9999

100-
// If a description is provided we generate an App Definition .codegenie directory based on it
101-
let appDir: string | undefined
100+
let appDir = cwd()
101+
102+
// If a description is provided we create a new directory using the name provided (--name)
103+
// or the AI-generated name, and generate an App Definition (.codegenie directory) within it based on the description
102104
if (description) {
103-
await this.handleExistingAppDefinition()
105+
// First check if we're within an existing Code Genie project directory by checking if a .codegnie directory already exists.
106+
// handleExistingAppDefinition MUST be run before generateAppDefinition, since generateAppDefinition creates a .codegenie directory.
107+
const hasExistingAppDefinition = await this.handleExistingAppDefinition()
104108
const generateAppDefinitionResult = await this.generateAppDefinition()
105-
if (generateAppDefinitionResult) {
106-
const { appName } = generateAppDefinitionResult
107-
appDir = getAppOutputDir({ appName })
109+
110+
// Usually we expect that `generate --description` is run NOT within an existing Code Genie project directory; therefore
111+
// hasExistingAppDefinition will usually be false. If `generate --description` is run within an existing Code Genie project directory,
112+
// handleExistingAppDefinition will throw unless --replaceAppDefinitionf is included, in which case we want `appDir` to remain as cwd().
113+
if (!hasExistingAppDefinition) {
114+
appDir = getAppOutputDir({ appName: generateAppDefinitionResult.appName })
108115
}
109-
return
110116
}
111117

112118
const { headObjectPresignedUrl, getObjectPresignedUrl } = await this.uploadAppDefinition({ appDir })
113-
await this.downloadS3OutputObject({
119+
120+
await this.downloadProject({
114121
headObjectPresignedUrl,
115122
getObjectPresignedUrl,
123+
appDir,
116124
})
117125

126+
const appName = await this.getAppName({ appDir })
127+
if (deploy && !awsCredentialsFileExists()) {
128+
const appDirRelative = getAppOutputDir({ appName, absolute: false })
129+
this.log(`The project has successfully been generated and downloaded to \`./${appDirRelative}\`.
130+
131+
The project wasn't able to be automatically built and deployed to AWS because the AWS CLI isn\'t set up on this machine. Install the AWS CLI and then run \`npm run init:dev\` inside the \`./${appDirRelative}\` directory.
132+
133+
For now you can open \`./${appDirRelative}\` in your favorite IDE like VS Code. Tip: You may even be able to simply run \`code ./${appDirRelative}\` to open it now.`)
134+
135+
return {
136+
description,
137+
deploy,
138+
awsProfileToCopy,
139+
appDir,
140+
}
141+
}
142+
118143
if (!noCopyAwsProfile) {
119-
const appName = await this.getAppName({ appDir })
120144
copyAwsProfile({ appName })
121145
}
122146

123147
if (deploy) {
124-
await this.runInitDev()
148+
await this.runInitDev({ appDir })
125149
}
126150

127151
return {
128152
description,
129153
deploy,
130154
awsProfileToCopy,
155+
appDir,
131156
}
132157
}
133158

@@ -155,13 +180,13 @@ generating app...
155180
const { flags } = await this.parse(Generate)
156181
const { replaceAppDefinition } = flags
157182

158-
if (!existsSync('.codegenie')) return
183+
if (!existsSync('.codegenie')) return false
159184

160185
if (replaceAppDefinition) {
161186
ux.action.start('Deleting .codegenie')
162187
rmSync('.codegenie', { recursive: true, force: true })
163188
ux.action.stop('✅')
164-
return
189+
return true
165190
}
166191

167192
this.error('A .codegenie directory already exists.', {
@@ -176,7 +201,7 @@ generating app...
176201
/**
177202
* Generates a [.codegenie app definition](https://codegenie.codes/docs) based on the provided description
178203
*/
179-
async generateAppDefinition(): Promise<void | { app: App; appName: string; appDescription: string }> {
204+
async generateAppDefinition(): Promise<{ app: App; appName: string; appDescription: string }> {
180205
const { flags } = await this.parse(Generate)
181206
const { description, name } = flags
182207
ux.action.start('🧞 Generating App Definition')
@@ -223,12 +248,12 @@ generating app...
223248
* @param root0
224249
* @param root0.appDir
225250
*/
226-
async uploadAppDefinition({ appDir }: { appDir?: string } = {}) {
251+
async uploadAppDefinition({ appDir }: { appDir: string }) {
227252
ux.action.start('⬆️📦 Uploading App Definition')
228253
const appName = await this.getAppName({ appDir })
229254
const output = await axios.get(`/build-upload-presigned-url?appName=${appName}`)
230255
const { putObjectPresignedUrl, headObjectPresignedUrl, getObjectPresignedUrl } = output.data.data
231-
const appDefinitionZip = await this.createZip(path.resolve(appDir || cwd(), '.codegenie'))
256+
const appDefinitionZip = await this.createZip(path.resolve(appDir, '.codegenie'))
232257
const zipFileSizeBytes = Buffer.byteLength(appDefinitionZip)
233258
const readable = getReadableFromBuffer(appDefinitionZip)
234259

@@ -305,12 +330,14 @@ generating app...
305330
}
306331
}
307332

308-
async downloadS3OutputObject({
333+
async downloadProject({
309334
headObjectPresignedUrl,
310335
getObjectPresignedUrl,
336+
appDir,
311337
}: {
312338
headObjectPresignedUrl: string
313339
getObjectPresignedUrl: string
340+
appDir: string
314341
}): Promise<undefined> {
315342
ux.action.start('🏗️ Generating project')
316343
await this.pollS3ObjectExistence({ headObjectPresignedUrl })
@@ -319,14 +346,15 @@ generating app...
319346
const response = await axios.get(getObjectPresignedUrl, { responseType: 'arraybuffer' })
320347
const zip = new AdmZip(response.data)
321348
const overwrite = false
322-
zip.extractAllTo('.', overwrite)
349+
zip.extractAllTo(appDir, overwrite)
323350
ux.action.stop('✅')
324351
}
325352

326-
async runInitDev(): Promise<undefined> {
353+
async runInitDev({ appDir }: { appDir: string }): Promise<undefined> {
327354
ux.action.start('🌩️ Deploying to AWS. The first deploy may take up to 10 minutes')
328355
execSync('npm run init:dev', {
329356
stdio: 'inherit',
357+
cwd: appDir,
330358
})
331359
ux.action.stop('✅')
332360
}

src/copyAwsProfile.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import fs from 'node:fs'
22
import path from 'node:path'
33
import os from 'node:os'
4-
import { paramCase } from './string-utils.js'
4+
import { kebabCase } from './string-utils.js'
5+
6+
const awsCredentialsFilePath = path.resolve(os.homedir(), '.aws/credentials')
7+
8+
export function awsCredentialsFileExists() {
9+
return fs.existsSync(awsCredentialsFilePath)
10+
}
511

612
export default function copyAwsProfile({ appName }: { appName: string }) {
7-
const awsCredentialsFile = path.resolve(os.homedir(), '.aws/credentials')
8-
const awsCredentials = fs.readFileSync(awsCredentialsFile, 'utf8')
9-
const newProfileNameWithoutEnvSuffix = paramCase(appName)
13+
// Backup the credentials file before we touch it just to be safe
14+
fs.copyFileSync(awsCredentialsFilePath, `${awsCredentialsFilePath}_cg.bak`)
15+
16+
const awsCredentials = fs.readFileSync(awsCredentialsFilePath, 'utf8')
17+
const newProfileNameWithoutEnvSuffix = kebabCase(appName)
1018
const newDevProfileName = `${newProfileNameWithoutEnvSuffix}_dev`
1119
const newStagingProfileName = `${newProfileNameWithoutEnvSuffix}_staging`
1220
const newProdProfileName = `${newProfileNameWithoutEnvSuffix}_prod`
@@ -18,7 +26,7 @@ export default function copyAwsProfile({ appName }: { appName: string }) {
1826
const [profileToCopyCreds] = profileToCopySplit.split(/\[.*]/)
1927
const profileToCopyCredsNewlineCleaned = profileToCopyCreds.replace('\n\n', '\n')
2028
fs.appendFileSync(
21-
awsCredentialsFile,
29+
awsCredentialsFilePath,
2230
`
2331
2432
[${newDevProfileName}]${profileToCopyCredsNewlineCleaned}

src/string-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ export function getEntityNameStrings({ entityName }: { entityName: string }) {
1212
}
1313
}
1414

15-
export const paramCase = (v: string) => _s.dasherize(_s.decapitalize(v))
15+
export const kebabCase = (v: string) => _s.dasherize(_s.decapitalize(v))

0 commit comments

Comments
 (0)