-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
improve link command #10257
improve link command #10257
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,60 @@ | ||
import { mkdir, writeFile } from 'node:fs/promises'; | ||
import type { AstroConfig } from 'astro'; | ||
import { slug } from 'github-slugger'; | ||
import { bgRed, cyan } from 'kleur/colors'; | ||
import { mkdir, writeFile } from 'node:fs/promises'; | ||
import { homedir } from 'node:os'; | ||
import { basename } from 'node:path'; | ||
import ora from 'ora'; | ||
import prompts from 'prompts'; | ||
import type { Arguments } from 'yargs-parser'; | ||
import { MISSING_SESSION_ID_ERROR } from '../../../errors.js'; | ||
import { PROJECT_ID_FILE, getSessionIdFromFile } from '../../../tokens.js'; | ||
import { getAstroStudioUrl } from '../../../utils.js'; | ||
|
||
export async function cmd({ flags }: { config: AstroConfig; flags: Arguments }) { | ||
const linkUrl = new URL(getAstroStudioUrl() + '/auth/cli/link'); | ||
export async function cmd({}: { config: AstroConfig; flags: Arguments }) { | ||
const sessionToken = await getSessionIdFromFile(); | ||
if (!sessionToken) { | ||
console.error(MISSING_SESSION_ID_ERROR); | ||
process.exit(1); | ||
} | ||
let body = { id: flags._[4] } as { | ||
id?: string; | ||
projectIdName?: string; | ||
workspaceIdName?: string; | ||
}; | ||
if (!body.id) { | ||
const workspaceIdName = await promptWorkspaceName(); | ||
const projectIdName = await promptProjectName(); | ||
body = { projectIdName, workspaceIdName }; | ||
const getWorkspaceIdAsync = getWorkspaceId(); | ||
await promptBegin(); | ||
const isLinkExisting = await promptLinkExisting(); | ||
if (isLinkExisting) { | ||
const workspaceId = await getWorkspaceIdAsync; | ||
const existingProjectData = await promptExistingProjectName({workspaceId}); | ||
return await linkProject(existingProjectData.id); | ||
} | ||
|
||
const isLinkNew = await promptLinkNew(); | ||
if (isLinkNew) { | ||
const workspaceId = await getWorkspaceIdAsync; | ||
const newProjectName = await promptNewProjectName(); | ||
const newProjectRegion = await promptNewProjectRegion(); | ||
const spinner = ora('Creating new project...').start(); | ||
const newProjectData = await createNewProject({workspaceId, name: newProjectName, region: newProjectRegion}); | ||
// TODO(fks): Actually listen for project creation before continuing | ||
// This is just a dumb spinner that roughly matches database creation time. | ||
await new Promise((r) => setTimeout(r, 4000)); | ||
spinner.succeed('Project created!'); | ||
return await linkProject(newProjectData.id); | ||
} | ||
} | ||
|
||
async function linkProject(id: string) { | ||
await mkdir(new URL('.', PROJECT_ID_FILE), { recursive: true }); | ||
await writeFile(PROJECT_ID_FILE, `${id}`); | ||
console.info('Project linked.'); | ||
} | ||
|
||
async function getWorkspaceId(): Promise<string> { | ||
const linkUrl = new URL(getAstroStudioUrl() + '/api/cli/workspaces.list'); | ||
const response = await fetch(linkUrl, { | ||
method: 'POST', | ||
headers: { | ||
Authorization: `Bearer ${await getSessionIdFromFile()}`, | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify(body), | ||
}); | ||
if (!response.ok) { | ||
// Unauthorized | ||
|
@@ -42,38 +66,164 @@ export async function cmd({ flags }: { config: AstroConfig; flags: Arguments }) | |
); | ||
process.exit(1); | ||
} | ||
console.error(`Failed to fetch user workspace: ${response.status} ${response.statusText}`); | ||
process.exit(1); | ||
} | ||
const { data, success } = await response.json() as {success: false, data: unknown} | {success: true, data: {id: string}[]}; | ||
if (!success) { | ||
console.error(`Failed to fetch user's workspace.`); | ||
process.exit(1); | ||
} | ||
return data[0].id; | ||
} | ||
|
||
console.error(`Failed to link project: ${response.status} ${response.statusText}`); | ||
export async function createNewProject({workspaceId, name, region}: {workspaceId: string; name: string, region: string}) { | ||
const linkUrl = new URL(getAstroStudioUrl() + '/api/cli/projects.create'); | ||
const response = await fetch(linkUrl, { | ||
method: 'POST', | ||
headers: { | ||
Authorization: `Bearer ${await getSessionIdFromFile()}`, | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify({ workspaceId, name, region }), | ||
}); | ||
if (!response.ok) { | ||
// Unauthorized | ||
if (response.status === 401) { | ||
console.error( | ||
`${bgRed('Unauthorized')}\n\n Are you logged in?\n Run ${cyan( | ||
'astro db login' | ||
)} to authenticate and then try linking again.\n\n` | ||
); | ||
process.exit(1); | ||
} | ||
console.error(`Failed to create project: ${response.status} ${response.statusText}`); | ||
process.exit(1); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we standardize to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah good catch, I think that already is the response format and this is just checking for the case where our entire API fails and you just get back an unhandled 500. But, we should also have proper handling for the handled error case. |
||
} | ||
const { data } = await response.json(); | ||
await mkdir(new URL('.', PROJECT_ID_FILE), { recursive: true }); | ||
await writeFile(PROJECT_ID_FILE, `${data.id}`); | ||
console.info('Project linked.'); | ||
const { data, success } = await response.json() as {success: false, data: unknown} | {success: true, data: {id: string; idName: string}}; | ||
if (!success) { | ||
console.error(`Failed to create project.`); | ||
process.exit(1); | ||
} | ||
return {id: data.id, idName: data.idName}; | ||
} | ||
|
||
export async function promptProjectName(defaultName?: string): Promise<string> { | ||
const { projectName } = await prompts({ | ||
type: 'text', | ||
name: 'projectName', | ||
message: 'Project ID', | ||
initial: defaultName, | ||
export async function promptExistingProjectName({workspaceId}: {workspaceId: string}) { | ||
const linkUrl = new URL(getAstroStudioUrl() + '/api/cli/projects.list'); | ||
const response = await fetch(linkUrl, { | ||
method: 'POST', | ||
headers: { | ||
Authorization: `Bearer ${await getSessionIdFromFile()}`, | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify({workspaceId}), | ||
}); | ||
if (!response.ok) { | ||
// Unauthorized | ||
if (response.status === 401) { | ||
console.error( | ||
`${bgRed('Unauthorized')}\n\n Are you logged in?\n Run ${cyan( | ||
'astro db login' | ||
)} to authenticate and then try linking again.\n\n` | ||
); | ||
process.exit(1); | ||
} | ||
console.error(`Failed to fetch projects: ${response.status} ${response.statusText}`); | ||
process.exit(1); | ||
} | ||
const { data, success } = await response.json() as {success: false, data: unknown} | {success: true, data: {id: string; idName: string}[]}; | ||
if (!success) { | ||
console.error(`Failed to fetch projects.`); | ||
process.exit(1); | ||
} | ||
const { projectId } = await prompts({ | ||
type: 'autocomplete', | ||
name: 'projectId', | ||
message: 'What is your project name?', | ||
limit: 5, | ||
choices: data.map((p: any) => ({title: p.name, value: p.id})), | ||
}); | ||
if (typeof projectName !== 'string') { | ||
if (typeof projectId !== 'string') { | ||
console.log('Canceled.') | ||
process.exit(0); | ||
} | ||
return projectName; | ||
const selectedProjectData = data.find((p: any) => p.id === projectId)!; | ||
return selectedProjectData; | ||
} | ||
|
||
export async function promptBegin(): Promise<void> { | ||
// Get the current working directory relative to the user's home directory | ||
const prettyCwd = process.cwd().replace(homedir(), '~'); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please work on windows please work on windows please work on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it should... right? |
||
// prompt | ||
const { begin } = await prompts({ | ||
type: 'confirm', | ||
name: 'begin', | ||
message: `Link "${prettyCwd}" with Astro Studio?`, | ||
initial: true, | ||
}); | ||
if (!begin) { | ||
console.log('Canceled.') | ||
process.exit(0); | ||
}; | ||
} | ||
|
||
export async function promptLinkExisting(): Promise<boolean> { | ||
// prompt | ||
const { linkExisting } = await prompts({ | ||
type: 'confirm', | ||
name: 'linkExisting', | ||
message: `Link with an existing project in Astro Studio?`, | ||
initial: true, | ||
}); | ||
return !!linkExisting; | ||
} | ||
|
||
export async function promptLinkNew(): Promise<boolean> { | ||
// prompt | ||
const { linkNew } = await prompts({ | ||
type: 'confirm', | ||
name: 'linkNew', | ||
message: `Create a new project in Astro Studio?`, | ||
initial: true, | ||
}); | ||
if (!linkNew) { | ||
console.log('Canceled.') | ||
process.exit(0); | ||
}; | ||
return true; | ||
} | ||
|
||
export async function promptWorkspaceName(defaultName?: string): Promise<string> { | ||
const { workspaceName } = await prompts({ | ||
|
||
export async function promptNewProjectName(): Promise<string> { | ||
const { newProjectName } = await prompts({ | ||
type: 'text', | ||
name: 'workspaceName', | ||
message: 'Workspace ID', | ||
initial: defaultName, | ||
name: 'newProjectName', | ||
message: `What is your new project's name?`, | ||
initial: basename(process.cwd()), | ||
format: (val) => slug(val), | ||
}); | ||
if (typeof workspaceName !== 'string') { | ||
if (!newProjectName) { | ||
console.log('Canceled.') | ||
process.exit(0); | ||
} | ||
return workspaceName; | ||
}; | ||
return newProjectName; | ||
} | ||
|
||
export async function promptNewProjectRegion(): Promise<string> { | ||
const { newProjectRegion } = await prompts({ | ||
type: 'select', | ||
name: 'newProjectRegion', | ||
message: `Where should your new database live?`, | ||
choices: [ | ||
{title: 'North America (East)', value: 'NorthAmericaEast'}, | ||
{title: 'North America (West)', value: 'NorthAmericaWest'} | ||
], | ||
initial: 0, | ||
}); | ||
if (!newProjectRegion) { | ||
console.log('Canceled.') | ||
process.exit(0); | ||
}; | ||
return newProjectRegion; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"First make it work, then make it good."
~ Confucius, probably
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When this is addressed, it would be great to provide a link to your project dashboard!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh nice, that's a great call. Just refactored the code a bit to make this even easier in the future