Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d0cfd07

Browse files
joshnussbalazsorban44
andauthoredJun 14, 2024
feat(add): init Guided provider setup (#7)
* Adds `auth add <provider>` command * Extracts logic for requiring framework * Adds framework detection to `auth add` * Refactors to use provider names from lib/meta * Computes urls for base and callbacks * Prompts and opens setup page in browser * Adds prompt to copy callback url to clipboard * Prompts for client id & secret and writes env * Fixes base url * Masks client secret input * suggestions --------- Co-authored-by: Balázs Orbán <[email protected]>
1 parent 9812fee commit d0cfd07

File tree

16 files changed

+567
-255
lines changed

16 files changed

+567
-255
lines changed
 

‎commands/add.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// @ts-check
2+
3+
import * as y from "yoctocolors"
4+
import open from "open"
5+
import clipboard from "clipboardy"
6+
import { select, input, password } from "@inquirer/prompts"
7+
import { requireFramework } from "../lib/detect.js"
8+
import { updateEnvFile } from "../lib/write-env.js"
9+
import { providers, frameworks } from "../lib/meta.js"
10+
import { secret } from "./index.js"
11+
import { link, markdownToAnsi } from "../lib/markdown.js"
12+
13+
const choices = Object.entries(providers)
14+
.filter(([, { setupUrl }]) => !!setupUrl)
15+
.map(([value, { name }]) => ({ name, value }))
16+
17+
/** @param {string | undefined} providerId */
18+
export async function action(providerId) {
19+
try {
20+
if (!providerId) {
21+
providerId = await select({
22+
message: "What provider do you want to set up?",
23+
choices: choices,
24+
})
25+
}
26+
27+
const provider = providers[providerId]
28+
if (!provider?.setupUrl) {
29+
console.error(
30+
y.red(
31+
`Missing instructions for ${
32+
provider?.name ?? providerId
33+
}.\nInstructions are available for: ${y.bold(
34+
choices.map((choice) => choice.name).join(", ")
35+
)}`
36+
)
37+
)
38+
return
39+
}
40+
41+
const frameworkId = await requireFramework()
42+
const framework = frameworks[frameworkId]
43+
44+
console.log(
45+
y.dim(
46+
`Setting up OAuth provider in your ${framework.name} app (${link(
47+
"more info",
48+
"https://authjs.dev/getting-started/authentication/oauth"
49+
)})...\n`
50+
)
51+
)
52+
53+
const url = new URL(
54+
`${framework.path}/callback/${providerId}`,
55+
`http://localhost:${framework.port}`
56+
)
57+
58+
clipboard.writeSync(url.toString())
59+
console.log(
60+
`\
61+
${y.bold("Setup URL")}: ${provider.setupUrl}
62+
${y.bold("Callback URL (copied to clipboard)")}: ${url}`
63+
)
64+
65+
console.log("_________________________")
66+
if (provider.instructions) {
67+
console.log(y.dim("\nFollow the instructions:\n"))
68+
console.log(markdownToAnsi(provider.instructions))
69+
}
70+
console.log(
71+
y.dim(
72+
`\nProvider documentation: https://providers.authjs.dev/${providerId}\n`
73+
)
74+
)
75+
console.log("‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾")
76+
console.log(y.dim("Opening setup URL in your browser...\n"))
77+
await new Promise((resolve) => setTimeout(resolve, 3000))
78+
79+
await open(provider.setupUrl)
80+
81+
const clientId = await input({
82+
message: `Paste ${y.magenta("Client ID")}:`,
83+
validate: (value) => !!value,
84+
})
85+
const clientSecret = await password({
86+
message: `Paste ${y.magenta("Client secret")}:`,
87+
mask: true,
88+
validate: (value) => !!value,
89+
})
90+
91+
console.log(y.dim(`Updating environment variable file...`))
92+
93+
const varPrefix = `AUTH_${providerId.toUpperCase()}`
94+
95+
await updateEnvFile({
96+
[`${varPrefix}_ID`]: clientId,
97+
[`${varPrefix}_SECRET`]: clientSecret,
98+
})
99+
100+
console.log(
101+
y.dim(
102+
`\nEnsuring that ${link(
103+
"AUTH_SECRET",
104+
"https://authjs.dev/getting-started/installation#setup-environment"
105+
)} is set...`
106+
)
107+
)
108+
109+
await secret.action({})
110+
111+
console.log("\n🎉 Done! You can now use this provider in your app.")
112+
} catch (error) {
113+
if (!(error instanceof Error)) return
114+
if (error.message.startsWith("User force closed")) return
115+
throw error
116+
}
117+
}

‎commands/ask.js

Lines changed: 1 addition & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { input } from "@inquirer/prompts"
55
import { InkeepAI } from "@inkeep/ai-api"
66
import { ChatModeOptions } from "@inkeep/ai-api/models/components/index.js"
77
import * as ora from "ora"
8+
import { link, markdownToAnsi, breakStringToLines } from "../lib/markdown.js"
89

910
const INKEEP_API_KEY = "e32967a320a48a2cd933922099e1f38f6ebb4ab62ff98343"
1011
const INKEEP_INTEGRATION_ID = "clvn0fdez000cip0e5w2oaobw"
@@ -87,60 +88,3 @@ export async function action({ stream = false, raw = false }) {
8788
spinner.stop().clear()
8889
}
8990
}
90-
91-
// double char markdown matchers
92-
const BOLD_REGEX = /\*{2}([^*]+)\*{2}/g
93-
const UNDERLINE_REGEX = /_{2}([^_]+)_{2}/g
94-
const STRIKETHROUGH_REGEX = /~{2}([^~]+)~{2}/g
95-
const LINK_REGEX = /\[([^\]]+)\]\(([^)]+)\)/g
96-
97-
// single char markdown matchers
98-
const ITALIC_REGEX = /(?<!\\)\*(.+)(?<!\\)\*|(?<!\\)_(.+)(?<!\\)_/g
99-
100-
function link(text, url) {
101-
return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`
102-
}
103-
104-
/**
105-
* @param {string} input
106-
* @returns {string}
107-
*/
108-
function markdownToAnsi(input) {
109-
input = input.replace(BOLD_REGEX, (...args) => y.bold(args[1]))
110-
input = input.replace(UNDERLINE_REGEX, (...args) => y.underline(args[1]))
111-
input = input.replace(STRIKETHROUGH_REGEX, (...args) =>
112-
y.strikethrough(args[1])
113-
)
114-
input = input.replace(ITALIC_REGEX, (...args) => y.italic(args[1] || args[2]))
115-
input = input.replace(/(?<!\\)\\/g, "")
116-
117-
// @ts-expect-error
118-
input = input.replaceAll(LINK_REGEX, (...args) =>
119-
y.blue(" " + link(args[2], args[1]))
120-
)
121-
return input
122-
}
123-
124-
/**
125-
* @param {string} str
126-
* @param {number} maxLineLength
127-
* @returns {string}
128-
*/
129-
function breakStringToLines(str, maxLineLength) {
130-
let result = ""
131-
let line = ""
132-
133-
str.split(" ").forEach((word) => {
134-
if (line.length + word.length + 1 > maxLineLength) {
135-
result += line + "\n"
136-
line = word
137-
} else {
138-
if (line) line += " "
139-
line += word
140-
}
141-
})
142-
143-
if (line) result += line
144-
145-
return result
146-
}

‎commands/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * as ask from "./ask.js"
22
export * as init from "./init.js"
33
export * as secret from "./secret.js"
4+
export * as add from './add.js'

‎commands/init.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export async function action(framework, options) {
3636
const providers = await checkbox({
3737
instructions: false,
3838
message: "Select one or multiple providers",
39-
choices: Object.entries(meta.providers).map(([value, name]) => ({
39+
choices: Object.entries(meta.providers).map(([value, { name }]) => ({
4040
name,
4141
value,
4242
})),
@@ -76,7 +76,7 @@ async function createFromExample(framework, dir) {
7676
})
7777
) {
7878
execSync(`cd ${dir}`)
79-
await secretAction({ write: true, path: dir })
79+
await secretAction({ path: dir })
8080
}
8181

8282
await install()
@@ -110,7 +110,12 @@ async function scaffoldProject(options) {
110110
for (const provider of providers) {
111111
const id = `AUTH_${provider.toUpperCase()}_ID`
112112
const secret = `AUTH_${provider.toUpperCase()}_SECRET`
113-
await updateEnvFile(dir, id, "")
114-
await updateEnvFile(dir, secret, "")
113+
await updateEnvFile(
114+
{
115+
[id]: "",
116+
[secret]: "",
117+
},
118+
dir
119+
)
115120
}
116121
}

‎commands/secret.js

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
// @ts-check
22

33
import * as y from "yoctocolors"
4-
import { write } from "../lib/clipboard/index.js"
5-
import { detectFramework } from "../lib/detect.js"
6-
import { join } from "node:path"
4+
import clipboard from "clipboardy"
5+
import { requireFramework } from "../lib/detect.js"
76
import { updateEnvFile } from "../lib/write-env.js"
8-
import { frameworks } from "../lib/meta.js"
97

108
/** Web compatible method to create a random string of a given length */
119
function randomString(size = 32) {
@@ -19,7 +17,6 @@ function randomString(size = 32) {
1917
* copy?: boolean
2018
* path?: string
2119
* raw?: boolean
22-
* write?:boolean
2320
* }} options
2421
*/
2522
export async function action(options) {
@@ -38,7 +35,7 @@ export async function action(options) {
3835

3936
if (options.copy) {
4037
try {
41-
write(line)
38+
clipboard.writeSync(line)
4239
console.log(message.introClipboard)
4340
} catch (error) {
4441
console.error(y.red(error))
@@ -50,16 +47,8 @@ export async function action(options) {
5047
}
5148

5249
try {
53-
const framework = await detectFramework(options.path)
54-
if (framework === "unknown") {
55-
return console.log(
56-
`No framework detected. Currently supported frameworks are: ${y.bold(
57-
Object.keys(frameworks).join(", ")
58-
)}`
59-
)
60-
}
61-
62-
await updateEnvFile(options.path, key, value)
50+
await requireFramework(options.path)
51+
await updateEnvFile({ [key]: value }, options.path, true)
6352
} catch (error) {
6453
console.error(y.red(error))
6554
}

‎index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import { Command, InvalidArgumentError } from "commander"
66
import * as y from "yoctocolors"
7-
import { ask, init, secret } from "./commands/index.js"
7+
import { ask, init, secret, add } from "./commands/index.js"
88

99
// import pkg from "./package.json" assert { type: "json" }
1010

@@ -52,6 +52,12 @@ program
5252
.description("Generate a random string and add it to the .env file.")
5353
.action(secret.action)
5454

55+
program
56+
.command("add")
57+
.argument("[provider]", "The authentication provider.")
58+
.description('Register a new authentication provider')
59+
.action(add.action)
60+
5561
program.parse()
5662

5763
export { program }

‎lib/clipboard/index.js

Lines changed: 0 additions & 14 deletions
This file was deleted.

‎lib/clipboard/linux.js

Lines changed: 0 additions & 33 deletions
This file was deleted.

‎lib/clipboard/macos.js

Lines changed: 0 additions & 11 deletions
This file was deleted.

‎lib/clipboard/windows.js

Lines changed: 0 additions & 8 deletions
This file was deleted.

‎lib/detect.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// @ts-check
22
import { readFile } from "node:fs/promises"
33
import { join } from "node:path"
4+
import { frameworks } from "../lib/meta.js"
5+
import * as y from "yoctocolors"
46

57
/**
68
* When this function runs in a framework directory we support,
@@ -35,3 +37,20 @@ export async function detectFramework(path = "") {
3537
return "unknown"
3638
}
3739
}
40+
41+
export async function requireFramework(path = "") {
42+
const framework = await detectFramework(path)
43+
44+
if (framework === "unknown") {
45+
console.error(
46+
y.red(`No framework detected. Currently supported frameworks are: ${y.bold(
47+
Object.keys(frameworks).join(", ")
48+
)}`)
49+
)
50+
51+
process.exit(0)
52+
}
53+
54+
return framework
55+
}
56+

‎lib/markdown.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// @ts-check
2+
import * as y from "yoctocolors"
3+
4+
// double char markdown matchers
5+
const BOLD_REGEX = /\*{2}([^*]+)\*{2}/g
6+
const UNDERLINE_REGEX = /_{2}([^_]+)_{2}/g
7+
const STRIKETHROUGH_REGEX = /~{2}([^~]+)~{2}/g
8+
const LINK_REGEX = /\[([^\]]+)\]\(([^)]+)\)/g
9+
10+
// single char markdown matchers
11+
const ITALIC_REGEX = /(?<!\\)\*(.+)(?<!\\)\*|(?<!\\)_(.+)(?<!\\)_/g
12+
13+
export function link(text, url) {
14+
return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`
15+
}
16+
17+
/**
18+
* @param {string} input
19+
* @returns {string}
20+
*/
21+
export function markdownToAnsi(input) {
22+
input = input.replace(BOLD_REGEX, (...args) => y.bold(args[1]))
23+
input = input.replace(UNDERLINE_REGEX, (...args) => y.underline(args[1]))
24+
input = input.replace(STRIKETHROUGH_REGEX, (...args) =>
25+
y.strikethrough(args[1])
26+
)
27+
input = input.replace(ITALIC_REGEX, (...args) => y.italic(args[1] || args[2]))
28+
input = input.replace(/(?<!\\)\\/g, "")
29+
30+
// @ts-expect-error
31+
input = input.replaceAll(LINK_REGEX, (...args) =>
32+
y.blue(" " + link(args[2], args[1]))
33+
)
34+
return input
35+
}
36+
37+
/**
38+
* @param {string} str
39+
* @param {number} maxLineLength
40+
* @returns {string}
41+
*/
42+
export function breakStringToLines(str, maxLineLength) {
43+
let result = ""
44+
let line = ""
45+
46+
str.split(" ").forEach((word) => {
47+
if (line.length + word.length + 1 > maxLineLength) {
48+
result += line + "\n"
49+
line = word
50+
} else {
51+
if (line) line += " "
52+
line += word
53+
}
54+
})
55+
56+
if (line) result += line
57+
58+
return result
59+
}

‎lib/meta.js

Lines changed: 115 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -11,106 +11,138 @@ export const frameworks = {
1111
name: "Next.js",
1212
src: "https://github.com/nextauthjs/next-auth-example",
1313
demo: "https://next-auth-example.vercel.app",
14+
path: "/api/auth",
15+
port: 3000,
1416
envFile: ".env.local",
1517
},
1618
sveltekit: {
1719
name: "SvelteKit",
1820
src: "https://github.com/nextauthjs/sveltekit-auth-example",
1921
demo: "https://sveltekit-auth-example.vercel.app",
22+
path: "/auth",
23+
port: 5173,
2024
envFile: ".env",
2125
},
2226
express: {
2327
name: "Express",
2428
src: "https://github.com/nextauthjs/express-auth-example",
2529
demo: "https://express-auth-example.vercel.app",
30+
path: "/auth",
31+
port: 3000,
2632
envFile: ".env",
2733
},
2834
}
2935

3036
export const providers = {
31-
"42-school": "42 School",
32-
apple: "Apple",
33-
asgardeo: "Asgardeo",
34-
auth0: "Auth0",
35-
authentik: "Authentik",
36-
"azure-ad-b2c": "Azure AD B2C",
37-
"azure-ad": "Azure AD",
38-
"azure-devops": "Azure DevOps",
39-
battlenet: "Battlenet",
40-
beyondidentity: "Beyond Identity",
41-
box: "Box",
42-
"boxyhq-saml": "Boxyhq SAML",
43-
bungie: "Bungie",
44-
"click-up": "Click-up",
45-
cognito: "Cognito",
46-
coinbase: "Coinbase",
47-
credentials: "Credentials",
48-
descope: "Descope",
49-
discord: "Discord",
50-
dribbble: "Dribbble",
51-
dropbox: "Dropbox",
52-
"duende-identity-server6": "Duende Identity Server 6",
53-
email: "Email",
54-
eveonline: "Eveonline",
55-
facebook: "Facebook",
56-
faceit: "Faceit",
57-
foursquare: "Foursquare",
58-
freshbooks: "Freshbooks",
59-
fusionauth: "FusionAuth",
60-
github: "GitHub",
61-
gitlab: "GitLab",
62-
google: "Google",
63-
hubspot: "Hubspot",
64-
"identity-server4": "Identity Server 4",
65-
instagram: "Instagram",
66-
kakao: "Kakao",
67-
keycloak: "Keycloak",
68-
line: "Line",
69-
linkedin: "LinkedIn",
70-
mailchimp: "Mailchimp",
71-
mailru: "Mail.ru",
72-
mastodon: "Mastodon",
73-
mattermost: "Mattermost",
74-
medium: "Medium",
75-
"microsoft-entra-id": "Microsoft Entra ID",
76-
naver: "Naver",
77-
netlify: "Netlify",
78-
netsuite: "Netsuite",
79-
nodemailer: "Nodemailer",
80-
notion: "Notion",
81-
okta: "Okta",
82-
onelogin: "Onelogin",
83-
"ory-hydra": "Ory Hydra",
84-
osso: "Osso",
85-
osu: "Osu",
86-
passage: "Passage",
87-
passkey: "Passkey",
88-
patreon: "Patreon",
89-
pinterest: "Pinterest",
90-
pipedrive: "Pipedrive",
91-
postmark: "Postmark",
92-
reddit: "Reddit",
93-
resend: "Resend",
94-
salesforce: "Salesforce",
95-
sendgrid: "Sendgrid",
96-
slack: "Slack",
97-
spotify: "Spotify",
98-
strava: "Strava",
99-
tiktok: "Tiktok",
100-
todoist: "Todoist",
101-
trakt: "Trakt",
102-
twitch: "Twitch",
103-
twitter: "Twitter",
104-
"united-effects": "United Effects",
105-
vk: "Vk",
106-
webex: "Webex",
107-
wikimedia: "Wikimedia",
108-
wordpress: "Wordpress",
109-
workos: "WorkOS",
110-
yandex: "Yandex",
111-
zitadel: "Zitadel",
112-
zoho: "Zoho",
113-
zoom: "Zoom",
37+
"42-school": { name: "42 School", setupUrl: undefined },
38+
apple: {
39+
name: "Apple",
40+
setupUrl:
41+
"https://developer.apple.com/account/resources/identifiers/list/serviceId",
42+
},
43+
asgardeo: { name: "Asgardeo", setupUrl: undefined },
44+
auth0: { name: "Auth0", setupUrl: undefined },
45+
authentik: { name: "Authentik", setupUrl: undefined },
46+
"azure-ad-b2c": { name: "Azure AD B2C", setupUrl: undefined },
47+
"azure-ad": { name: "Azure AD", setupUrl: undefined },
48+
"azure-devops": { name: "Azure DevOps", setupUrl: undefined },
49+
battlenet: { name: "Battlenet", setupUrl: undefined },
50+
beyondidentity: { name: "Beyond Identity", setupUrl: undefined },
51+
box: { name: "Box", setupUrl: undefined },
52+
"boxyhq-saml": { name: "Boxyhq SAML", setupUrl: undefined },
53+
bungie: { name: "Bungie", setupUrl: undefined },
54+
"click-up": { name: "Click-up", setupUrl: undefined },
55+
cognito: { name: "Cognito", setupUrl: undefined },
56+
coinbase: { name: "Coinbase", setupUrl: undefined },
57+
credentials: { name: "Credentials", setupUrl: undefined },
58+
descope: { name: "Descope", setupUrl: undefined },
59+
discord: { name: "Discord", setupUrl: undefined },
60+
dribbble: { name: "Dribbble", setupUrl: undefined },
61+
dropbox: { name: "Dropbox", setupUrl: undefined },
62+
"duende-identity-server6": {
63+
name: "Duende Identity Server 6",
64+
setupUrl: undefined,
65+
},
66+
email: { name: "Email", setupUrl: undefined },
67+
eveonline: { name: "Eveonline", setupUrl: undefined },
68+
facebook: { name: "Facebook", setupUrl: undefined },
69+
faceit: { name: "Faceit", setupUrl: undefined },
70+
foursquare: { name: "Foursquare", setupUrl: undefined },
71+
freshbooks: { name: "Freshbooks", setupUrl: undefined },
72+
fusionauth: { name: "FusionAuth", setupUrl: undefined },
73+
github: {
74+
name: "GitHub",
75+
setupUrl: "https://github.com/settings/applications/new",
76+
instructions: `\
77+
1. Set *Application name* (can be anything)
78+
2. Set *Homepage URL* (your business/website, but can be anything)
79+
3. Paste the redirect URI (on your clipboard) to *Authorization callback URL*
80+
4. Click *Register application*
81+
5. Paste the *Client ID* back here
82+
6. Click *Generate a new client secret*
83+
7. Paste the *Client secret* back here (Note: This is the only time you can see it)`,
84+
},
85+
gitlab: { name: "GitLab", setupUrl: undefined },
86+
google: {
87+
name: "Google",
88+
setupUrl: "https://console.cloud.google.com/apis/credentials/oauthclient",
89+
instructions: `\
90+
1. Choose *Application Type: Web Application*
91+
2. Paste the redirect URI (on your clipboard) to *Authorized redirect URIs*
92+
3. Fill out the rest of the form
93+
4. Click *Create*`,
94+
},
95+
hubspot: { name: "Hubspot", setupUrl: undefined },
96+
"identity-server4": { name: "Identity Server 4", setupUrl: undefined },
97+
instagram: { name: "Instagram", setupUrl: undefined },
98+
kakao: { name: "Kakao", setupUrl: undefined },
99+
keycloak: { name: "Keycloak", setupUrl: undefined },
100+
line: { name: "Line", setupUrl: undefined },
101+
linkedin: { name: "LinkedIn", setupUrl: undefined },
102+
mailchimp: { name: "Mailchimp", setupUrl: undefined },
103+
mailru: { name: "Mail.ru", setupUrl: undefined },
104+
mastodon: { name: "Mastodon", setupUrl: undefined },
105+
mattermost: { name: "Mattermost", setupUrl: undefined },
106+
medium: { name: "Medium", setupUrl: undefined },
107+
"microsoft-entra-id": { name: "Microsoft Entra ID", setupUrl: undefined },
108+
naver: { name: "Naver", setupUrl: undefined },
109+
netlify: { name: "Netlify", setupUrl: undefined },
110+
netsuite: { name: "Netsuite", setupUrl: undefined },
111+
nodemailer: { name: "Nodemailer", setupUrl: undefined },
112+
notion: { name: "Notion", setupUrl: undefined },
113+
okta: { name: "Okta", setupUrl: undefined },
114+
onelogin: { name: "Onelogin", setupUrl: undefined },
115+
"ory-hydra": { name: "Ory Hydra", setupUrl: undefined },
116+
osso: { name: "Osso", setupUrl: undefined },
117+
osu: { name: "Osu", setupUrl: undefined },
118+
passage: { name: "Passage", setupUrl: undefined },
119+
passkey: { name: "Passkey", setupUrl: undefined },
120+
patreon: { name: "Patreon", setupUrl: undefined },
121+
pinterest: { name: "Pinterest", setupUrl: undefined },
122+
pipedrive: { name: "Pipedrive", setupUrl: undefined },
123+
postmark: { name: "Postmark", setupUrl: undefined },
124+
reddit: { name: "Reddit", setupUrl: undefined },
125+
resend: { name: "Resend", setupUrl: undefined },
126+
salesforce: { name: "Salesforce", setupUrl: undefined },
127+
sendgrid: { name: "Sendgrid", setupUrl: undefined },
128+
slack: { name: "Slack", setupUrl: undefined },
129+
spotify: { name: "Spotify", setupUrl: undefined },
130+
strava: { name: "Strava", setupUrl: undefined },
131+
tiktok: { name: "Tiktok", setupUrl: undefined },
132+
todoist: { name: "Todoist", setupUrl: undefined },
133+
trakt: { name: "Trakt", setupUrl: undefined },
134+
twitch: { name: "Twitch", setupUrl: undefined },
135+
twitter: { name: "Twitter", setupUrl: undefined },
136+
"united-effects": { name: "United Effects", setupUrl: undefined },
137+
vk: { name: "Vk", setupUrl: undefined },
138+
webex: { name: "Webex", setupUrl: undefined },
139+
wikimedia: { name: "Wikimedia", setupUrl: undefined },
140+
wordpress: { name: "Wordpress", setupUrl: undefined },
141+
workos: { name: "WorkOS", setupUrl: undefined },
142+
yandex: { name: "Yandex", setupUrl: undefined },
143+
zitadel: { name: "Zitadel", setupUrl: undefined },
144+
zoho: { name: "Zoho", setupUrl: undefined },
145+
zoom: { name: "Zoom", setupUrl: undefined },
114146
}
115147

116148
export const adapters = {

‎lib/write-env.js

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,39 +8,53 @@ import { frameworks } from "./meta.js"
88
import { detectFramework } from "./detect.js"
99

1010
/**
11-
* Update a key-value pair to a .env file
11+
* Add/update key-value pair(s) to a .env file
12+
* @param {Record<string, string>} env
1213
* @param {string|undefined} envPath
13-
* @param {string} key
14-
* @param {string} value
14+
* @param {boolean} comment
1515
*/
16-
export async function updateEnvFile(envPath = "", key, value) {
16+
export async function updateEnvFile(env, envPath = "", comment = false) {
1717
const framework = await detectFramework(envPath)
1818
const dotEnvFile = frameworks[framework]?.envFile
19-
let content = ""
20-
const line = `${key}="${value}" # Added by \`npx auth\`. Read more: https://cli.authjs.dev`
2119
const file = join(process.cwd(), envPath, dotEnvFile)
22-
try {
23-
content = await readFile(file, "utf-8")
24-
if (!content.includes(`${key}=`)) {
25-
console.log(`➕ Added ${key} to ${y.italic(file)}.`)
26-
content = `${line}\n${content}`
27-
} else {
28-
const { overwrite } = await prompt({
29-
type: "confirm",
30-
name: "overwrite",
31-
message: `Overwrite existing ${key}?`,
32-
initial: false,
33-
})
34-
if (!overwrite) return
35-
console.log(`✨ Updated ${key} in ${y.italic(file)}.`)
36-
content = content.replace(new RegExp(`${key}=(.*)`), `${line}`)
37-
}
38-
} catch (error) {
39-
if (error.code === "ENOENT") {
40-
console.log(`📝 Created ${y.italic(file)} with ${key}.`)
41-
content = line
42-
} else {
43-
throw error
20+
let content = ""
21+
let read = false
22+
let created = false
23+
for (const [key, value] of Object.entries(env)) {
24+
const line = `${key}="${value}"${
25+
comment ? " # Added by `npx auth`. Read more: https://cli.authjs.dev" : ""
26+
}`
27+
try {
28+
if (!read) {
29+
content = await readFile(file, "utf-8")
30+
read = true
31+
}
32+
if (!content.includes(`${key}=`)) {
33+
console.log(`➕ Added \`${key}\` to ${y.italic(file)}.`)
34+
content = content ? `${content}\n${line}` : line
35+
} else {
36+
const { overwrite } = await prompt({
37+
type: "confirm",
38+
name: "overwrite",
39+
message: `Overwrite existing \`${key}\`?`,
40+
initial: false,
41+
})
42+
if (!overwrite) continue
43+
console.log(`✨ Updated \`${key}\` in ${y.italic(file)}.`)
44+
content = content.replace(new RegExp(`${key}=(.*)`), `${line}`)
45+
}
46+
} catch (error) {
47+
if (error.code === "ENOENT") {
48+
if (!created) {
49+
console.log(`📝 Created ${y.italic(file)} with \`${key}\`.`)
50+
created = true
51+
} else {
52+
console.log(`➕ Added \`${key}\` to ${y.italic(file)}.`)
53+
}
54+
content = content ? `${content}\n${line}` : line
55+
} else {
56+
throw error
57+
}
4458
}
4559
}
4660
if (content) await writeFile(file, content)

‎package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@
3636
"dependencies": {
3737
"@inkeep/ai-api": "^0.7.2",
3838
"@inquirer/prompts": "3.3.2",
39+
"clipboardy": "^4.0.0",
40+
"next": "0",
3941
"commander": "11.1.0",
42+
"open": "^10.1.0",
4043
"ora": "^8.0.1",
4144
"prompts": "^2.4.2",
4245
"yoctocolors": "1.0.0"

‎pnpm-lock.yaml

Lines changed: 189 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.