Skip to content

feat(add): init Guided provider setup #7

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

Merged
merged 11 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions commands/add.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// @ts-check

import * as y from "yoctocolors"
import open from "open"
import clipboard from "clipboardy"
import { select, input, password } from "@inquirer/prompts"
import { requireFramework } from "../lib/detect.js"
import { updateEnvFile } from "../lib/write-env.js"
import { providers, frameworks } from "../lib/meta.js"
import { secret } from "./index.js"
import { link, markdownToAnsi } from "../lib/markdown.js"

const choices = Object.entries(providers)
.filter(([, { setupUrl }]) => !!setupUrl)
.map(([value, { name }]) => ({ name, value }))

/** @param {string | undefined} providerId */
export async function action(providerId) {
try {
if (!providerId) {
providerId = await select({
message: "What provider do you want to set up?",
choices: choices,
})
}

const provider = providers[providerId]
if (!provider?.setupUrl) {
console.error(
y.red(
`Missing instructions for ${
provider?.name ?? providerId
}.\nInstructions are available for: ${y.bold(
choices.map((choice) => choice.name).join(", ")
)}`
)
)
return
}

const frameworkId = await requireFramework()
const framework = frameworks[frameworkId]

console.log(
y.dim(
`Setting up OAuth provider in your ${framework.name} app (${link(
"more info",
"https://authjs.dev/getting-started/authentication/oauth"
)})...\n`
)
)

const url = new URL(
`${framework.path}/callback/${providerId}`,
`http://localhost:${framework.port}`
)

clipboard.writeSync(url.toString())
console.log(
`\
${y.bold("Setup URL")}: ${provider.setupUrl}
${y.bold("Callback URL (copied to clipboard)")}: ${url}`
)

console.log("_________________________")
if (provider.instructions) {
console.log(y.dim("\nFollow the instructions:\n"))
console.log(markdownToAnsi(provider.instructions))
}
console.log(
y.dim(
`\nProvider documentation: https://providers.authjs.dev/${providerId}\n`
)
)
console.log("‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾")
console.log(y.dim("Opening setup URL in your browser...\n"))
await new Promise((resolve) => setTimeout(resolve, 3000))

await open(provider.setupUrl)

const clientId = await input({
message: `Paste ${y.magenta("Client ID")}:`,
validate: (value) => !!value,
})
const clientSecret = await password({
message: `Paste ${y.magenta("Client secret")}:`,
mask: true,
validate: (value) => !!value,
})

console.log(y.dim(`Updating environment variable file...`))

const varPrefix = `AUTH_${providerId.toUpperCase()}`

await updateEnvFile({
[`${varPrefix}_ID`]: clientId,
[`${varPrefix}_SECRET`]: clientSecret,
})

console.log(
y.dim(
`\nEnsuring that ${link(
"AUTH_SECRET",
"https://authjs.dev/getting-started/installation#setup-environment"
)} is set...`
)
)

await secret.action({})

console.log("\n🎉 Done! You can now use this provider in your app.")
} catch (error) {
if (!(error instanceof Error)) return
if (error.message.startsWith("User force closed")) return
throw error
}
}
58 changes: 1 addition & 57 deletions commands/ask.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { input } from "@inquirer/prompts"
import { InkeepAI } from "@inkeep/ai-api"
import { ChatModeOptions } from "@inkeep/ai-api/models/components/index.js"
import * as ora from "ora"
import { link, markdownToAnsi, breakStringToLines } from "../lib/markdown.js"

const INKEEP_API_KEY = "e32967a320a48a2cd933922099e1f38f6ebb4ab62ff98343"
const INKEEP_INTEGRATION_ID = "clvn0fdez000cip0e5w2oaobw"
Expand Down Expand Up @@ -87,60 +88,3 @@ export async function action({ stream = false, raw = false }) {
spinner.stop().clear()
}
}

// double char markdown matchers
const BOLD_REGEX = /\*{2}([^*]+)\*{2}/g
const UNDERLINE_REGEX = /_{2}([^_]+)_{2}/g
const STRIKETHROUGH_REGEX = /~{2}([^~]+)~{2}/g
const LINK_REGEX = /\[([^\]]+)\]\(([^)]+)\)/g

// single char markdown matchers
const ITALIC_REGEX = /(?<!\\)\*(.+)(?<!\\)\*|(?<!\\)_(.+)(?<!\\)_/g

function link(text, url) {
return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`
}

/**
* @param {string} input
* @returns {string}
*/
function markdownToAnsi(input) {
input = input.replace(BOLD_REGEX, (...args) => y.bold(args[1]))
input = input.replace(UNDERLINE_REGEX, (...args) => y.underline(args[1]))
input = input.replace(STRIKETHROUGH_REGEX, (...args) =>
y.strikethrough(args[1])
)
input = input.replace(ITALIC_REGEX, (...args) => y.italic(args[1] || args[2]))
input = input.replace(/(?<!\\)\\/g, "")

// @ts-expect-error
input = input.replaceAll(LINK_REGEX, (...args) =>
y.blue(" " + link(args[2], args[1]))
)
return input
}

/**
* @param {string} str
* @param {number} maxLineLength
* @returns {string}
*/
function breakStringToLines(str, maxLineLength) {
let result = ""
let line = ""

str.split(" ").forEach((word) => {
if (line.length + word.length + 1 > maxLineLength) {
result += line + "\n"
line = word
} else {
if (line) line += " "
line += word
}
})

if (line) result += line

return result
}
1 change: 1 addition & 0 deletions commands/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * as ask from "./ask.js"
export * as init from "./init.js"
export * as secret from "./secret.js"
export * as add from './add.js'
13 changes: 9 additions & 4 deletions commands/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export async function action(framework, options) {
const providers = await checkbox({
instructions: false,
message: "Select one or multiple providers",
choices: Object.entries(meta.providers).map(([value, name]) => ({
choices: Object.entries(meta.providers).map(([value, { name }]) => ({
name,
value,
})),
Expand Down Expand Up @@ -76,7 +76,7 @@ async function createFromExample(framework, dir) {
})
) {
execSync(`cd ${dir}`)
await secretAction({ write: true, path: dir })
await secretAction({ path: dir })
}

await install()
Expand Down Expand Up @@ -110,7 +110,12 @@ async function scaffoldProject(options) {
for (const provider of providers) {
const id = `AUTH_${provider.toUpperCase()}_ID`
const secret = `AUTH_${provider.toUpperCase()}_SECRET`
await updateEnvFile(dir, id, "")
await updateEnvFile(dir, secret, "")
await updateEnvFile(
{
[id]: "",
[secret]: "",
},
dir
)
}
}
21 changes: 5 additions & 16 deletions commands/secret.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
// @ts-check

import * as y from "yoctocolors"
import { write } from "../lib/clipboard/index.js"
import { detectFramework } from "../lib/detect.js"
import { join } from "node:path"
import clipboard from "clipboardy"
import { requireFramework } from "../lib/detect.js"
import { updateEnvFile } from "../lib/write-env.js"
import { frameworks } from "../lib/meta.js"

/** Web compatible method to create a random string of a given length */
function randomString(size = 32) {
Expand All @@ -19,7 +17,6 @@ function randomString(size = 32) {
* copy?: boolean
* path?: string
* raw?: boolean
* write?:boolean
* }} options
*/
export async function action(options) {
Expand All @@ -38,7 +35,7 @@ export async function action(options) {

if (options.copy) {
try {
write(line)
clipboard.writeSync(line)
console.log(message.introClipboard)
} catch (error) {
console.error(y.red(error))
Expand All @@ -50,16 +47,8 @@ export async function action(options) {
}

try {
const framework = await detectFramework(options.path)
if (framework === "unknown") {
return console.log(
`No framework detected. Currently supported frameworks are: ${y.bold(
Object.keys(frameworks).join(", ")
)}`
)
}

await updateEnvFile(options.path, key, value)
await requireFramework(options.path)
await updateEnvFile({ [key]: value }, options.path, true)
} catch (error) {
console.error(y.red(error))
}
Expand Down
8 changes: 7 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { Command, InvalidArgumentError } from "commander"
import * as y from "yoctocolors"
import { ask, init, secret } from "./commands/index.js"
import { ask, init, secret, add } from "./commands/index.js"

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

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

program
.command("add")
.argument("[provider]", "The authentication provider.")
.description('Register a new authentication provider')
.action(add.action)

program.parse()

export { program }
14 changes: 0 additions & 14 deletions lib/clipboard/index.js

This file was deleted.

33 changes: 0 additions & 33 deletions lib/clipboard/linux.js

This file was deleted.

11 changes: 0 additions & 11 deletions lib/clipboard/macos.js

This file was deleted.

8 changes: 0 additions & 8 deletions lib/clipboard/windows.js

This file was deleted.

19 changes: 19 additions & 0 deletions lib/detect.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// @ts-check
import { readFile } from "node:fs/promises"
import { join } from "node:path"
import { frameworks } from "../lib/meta.js"
import * as y from "yoctocolors"

/**
* When this function runs in a framework directory we support,
Expand Down Expand Up @@ -35,3 +37,20 @@ export async function detectFramework(path = "") {
return "unknown"
}
}

export async function requireFramework(path = "") {
const framework = await detectFramework(path)

if (framework === "unknown") {
console.error(
y.red(`No framework detected. Currently supported frameworks are: ${y.bold(
Object.keys(frameworks).join(", ")
)}`)
)

process.exit(0)
}

return framework
}

Loading