Skip to content

Commit 7c1877a

Browse files
committed
add with accounts extra
1 parent 4a4469f commit 7c1877a

File tree

254 files changed

+43722
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

254 files changed

+43722
-0
lines changed

extra/04.with-accounts/.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Optional (preview uses request origin by default)
2+
APP_BASE_URL=
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Example environment variables for local dev, test, and preview deploys.
2+
# Copy this to `.env` for local development and local tests.
3+
4+
# Optional (preview uses request origin by default)
5+
APP_BASE_URL=
6+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"$schema": "./node_modules/oxlint/configuration_schema.json",
3+
"extends": [
4+
"./node_modules/@epic-web/config/oxlint-config.json",
5+
"./tools/oxlint/oxlint-rules.json"
6+
]
7+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
node_modules
2+
dist
3+
build
4+
.next
5+
.vercel
6+
.netlify
7+
coverage
8+
*.log
9+
.env*
10+
!.env.example
11+
package-lock.json
12+
yarn.lock
13+
pnpm-lock.yaml
14+
**/worker-configuration.d.ts
15+
public/client-entry.js

extra/04.with-accounts/AGENTS.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# epic-scheduler agent index
2+
3+
Use Node.js v24+ and npm for installs/scripts (`npm install`, `npm run ...`).
4+
5+
## Code style
6+
7+
- Read and follow `docs/agents/code-style.md` before writing code.
8+
- Match the surrounding file style (quotes, semicolons, formatting).
9+
10+
This file is intentionally brief. Detailed instructions live in focused docs:
11+
12+
- Setup, checks, docs maintenance, preview deploys, and seeding:
13+
- [docs/agents/setup.md](./docs/agents/setup.md)
14+
- Code style conventions:
15+
- [docs/agents/code-style.md](./docs/agents/code-style.md)
16+
- Testing guidance:
17+
- [docs/agents/testing-principles.md](./docs/agents/testing-principles.md)
18+
- [docs/agents/end-to-end-testing.md](./docs/agents/end-to-end-testing.md)
19+
- Tooling and framework references:
20+
- [docs/agents/harness-engineering.md](./docs/agents/harness-engineering.md)
21+
- [docs/agents/oxlint-js-plugins.md](./docs/agents/oxlint-js-plugins.md)
22+
- [docs/agents/remix/index.md](./docs/agents/remix/index.md)
23+
- [docs/agents/cloudflare-agents-sdk.md](./docs/agents/cloudflare-agents-sdk.md)
24+
- [docs/agents/mcp-apps-starter-guide.md](./docs/agents/mcp-apps-starter-guide.md)
25+
- Project setup references:
26+
- [docs/getting-started.md](./docs/getting-started.md)
27+
- [docs/environment-variables.md](./docs/environment-variables.md)
28+
- [docs/setup-manifest.md](./docs/setup-manifest.md)
29+
- Architecture references:
30+
- [docs/architecture/index.md](./docs/architecture/index.md)
31+
- [docs/architecture/request-lifecycle.md](./docs/architecture/request-lifecycle.md)
32+
- [docs/architecture/authentication.md](./docs/architecture/authentication.md)
33+
- [docs/architecture/data-storage.md](./docs/architecture/data-storage.md)

extra/04.with-accounts/README.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# With Accounts
2+
3+
This is the project after you've added support for accounts.

extra/04.with-accounts/cli.ts

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
import { spawn, type ChildProcess } from 'node:child_process'
2+
import { platform } from 'node:os'
3+
import readline from 'node:readline'
4+
import { setTimeout as delay } from 'node:timers/promises'
5+
import getPort, { clearLockedPorts } from 'get-port'
6+
7+
const defaultWorkerPort = 3742
8+
9+
const ansiReset = '\x1b[0m'
10+
const ansiBright = '\x1b[1m'
11+
const ansiDim = '\x1b[2m'
12+
13+
function colorize(text: string, color: string) {
14+
const colorCode = ansiColorByName[color] ?? ''
15+
return colorCode ? `${colorCode}${text}${ansiReset}` : text
16+
}
17+
18+
function bright(text: string) {
19+
return `${ansiBright}${text}${ansiReset}`
20+
}
21+
22+
function dim(text: string) {
23+
return `${ansiDim}${text}${ansiReset}`
24+
}
25+
26+
type OutputFilterKey = 'client' | 'worker' | 'default'
27+
28+
const outputFilters: Record<OutputFilterKey, Array<RegExp>> = {
29+
client: [],
30+
worker: [],
31+
default: [],
32+
}
33+
34+
const extraArgs = process.argv.slice(2)
35+
let shutdown: (() => void) | null = null
36+
let devChildren: Array<ChildProcess> = []
37+
let workerOrigin = ''
38+
39+
const ansiColorByName: Record<string, string> = {
40+
cyan: '\x1b[36m',
41+
green: '\x1b[32m',
42+
cornflowerblue: '\x1b[94m',
43+
yellow: '\x1b[33m',
44+
orange: '\x1b[38;5;214m',
45+
magenta: '\x1b[35m',
46+
firebrick: '\x1b[31m',
47+
}
48+
49+
void startDev()
50+
51+
async function startDev() {
52+
await restartDev({ announce: false })
53+
setupInteractiveCli({
54+
getWorkerOrigin: () => workerOrigin,
55+
restart: restartDev,
56+
})
57+
shutdown = setupShutdown(() => devChildren)
58+
}
59+
60+
function resolveWorkerOrigin(port: number) {
61+
const envOrigin = process.env.WORKER_DEV_ORIGIN
62+
if (envOrigin) return envOrigin.trim()
63+
return `http://localhost:${port}`
64+
}
65+
66+
function runNpmScript(
67+
script: string,
68+
args: Array<string> = [],
69+
envOverrides: Record<string, string> = {},
70+
options: { outputFilter?: OutputFilterKey } = {},
71+
): ChildProcess {
72+
const npm = platform() === 'win32' ? 'npm.cmd' : 'npm'
73+
const child = spawn(npm, ['run', '--silent', script, '--', ...args], {
74+
stdio: ['inherit', 'pipe', 'pipe'],
75+
env: { ...process.env, ...envOverrides },
76+
})
77+
78+
pipeOutput(child, options.outputFilter)
79+
80+
child.on('exit', (code, signal) => {
81+
if (signal) return
82+
if (code && code !== 0) {
83+
process.exitCode = code
84+
}
85+
})
86+
87+
return child
88+
}
89+
90+
function pipeOutput(
91+
child: ChildProcess,
92+
filterKey: OutputFilterKey = 'default',
93+
) {
94+
const filters = outputFilters[filterKey]
95+
if (child.stdout) {
96+
pipeStream(child.stdout, process.stdout, filters)
97+
}
98+
if (child.stderr) {
99+
pipeStream(child.stderr, process.stderr, filters)
100+
}
101+
}
102+
103+
function pipeStream(
104+
source: NodeJS.ReadableStream,
105+
target: NodeJS.WritableStream,
106+
filters: Array<RegExp>,
107+
) {
108+
const rl = readline.createInterface({ input: source })
109+
rl.on('line', (line) => {
110+
if (filters.some((filter) => filter.test(line))) {
111+
return
112+
}
113+
target.write(`${line}\n`)
114+
})
115+
}
116+
117+
function setupShutdown(getChildren: () => Array<ChildProcess>) {
118+
let isShuttingDown = false
119+
function doShutdown() {
120+
if (isShuttingDown) return
121+
isShuttingDown = true
122+
console.log(dim('\nShutting down...'))
123+
const children = getChildren().filter((child) => child.exitCode === null)
124+
void (async () => {
125+
await Promise.all(children.map((child) => stopChild(child)))
126+
process.exit(0)
127+
})()
128+
}
129+
130+
process.on('SIGINT', doShutdown)
131+
process.on('SIGTERM', doShutdown)
132+
return doShutdown
133+
}
134+
135+
function setupInteractiveCli(options: {
136+
getWorkerOrigin: () => string
137+
restart: () => Promise<void>
138+
}) {
139+
const stdin = process.stdin
140+
if (!stdin.isTTY || typeof stdin.setRawMode !== 'function') return
141+
142+
showHelp()
143+
logAppRunning(options.getWorkerOrigin)
144+
145+
readline.emitKeypressEvents(stdin)
146+
stdin.setRawMode(true)
147+
stdin.resume()
148+
149+
stdin.on('keypress', (_key, key) => {
150+
if (key?.ctrl && key.name === 'c') {
151+
shutdown?.()
152+
return
153+
}
154+
155+
if (key?.name === 'return') {
156+
process.stdout.write('\n')
157+
return
158+
}
159+
160+
switch (key?.name) {
161+
case 'o': {
162+
openInBrowser(options.getWorkerOrigin())
163+
break
164+
}
165+
case 'u': {
166+
copyToClipboard(options.getWorkerOrigin())
167+
break
168+
}
169+
case 'c': {
170+
console.clear()
171+
showHelp()
172+
logAppRunning(options.getWorkerOrigin)
173+
break
174+
}
175+
case 'r': {
176+
void options.restart()
177+
break
178+
}
179+
case 'h':
180+
case '?': {
181+
showHelp()
182+
break
183+
}
184+
case 'q': {
185+
shutdown?.()
186+
break
187+
}
188+
}
189+
})
190+
}
191+
192+
function showHelp(header?: string) {
193+
if (header) console.log(header)
194+
console.log(`\n${bright('CLI shortcuts:')}`)
195+
console.log(
196+
` ${colorize('o', 'cyan')} - ${colorize('open browser', 'green')}`,
197+
)
198+
console.log(
199+
` ${colorize('u', 'cyan')} - ${colorize('copy URL', 'cornflowerblue')}`,
200+
)
201+
console.log(
202+
` ${colorize('c', 'cyan')} - ${colorize('clear console', 'yellow')}`,
203+
)
204+
console.log(` ${colorize('r', 'cyan')} - ${colorize('restart', 'orange')}`)
205+
console.log(` ${colorize('h', 'cyan')} - ${colorize('help', 'magenta')}`)
206+
console.log(` ${colorize('q', 'cyan')} - ${colorize('quit', 'firebrick')}`)
207+
}
208+
209+
async function restartDev(
210+
{ announce }: { announce: boolean } = { announce: true },
211+
) {
212+
await stopChildren(devChildren)
213+
const desiredPort = Number.parseInt(
214+
process.env.PORT ?? String(defaultWorkerPort),
215+
10,
216+
)
217+
const portRange = Array.from(
218+
{ length: 10 },
219+
(_, index) => desiredPort + index,
220+
)
221+
clearLockedPorts()
222+
const workerPort = await getPort({ port: portRange })
223+
workerOrigin = resolveWorkerOrigin(workerPort)
224+
const client = runNpmScript(
225+
'dev:client',
226+
[],
227+
{},
228+
{
229+
outputFilter: 'client',
230+
},
231+
)
232+
const worker = runNpmScript(
233+
'dev:worker',
234+
extraArgs,
235+
{ PORT: String(workerPort) },
236+
{ outputFilter: 'worker' },
237+
)
238+
devChildren = [client, worker]
239+
240+
if (announce) {
241+
console.log(dim('\nRestarted dev servers.'))
242+
logAppRunning(() => workerOrigin)
243+
}
244+
}
245+
246+
async function stopChildren(children: Array<ChildProcess>) {
247+
await Promise.all(children.map((child) => stopChild(child)))
248+
}
249+
250+
async function stopChild(child: ChildProcess) {
251+
if (child.killed) return
252+
child.kill('SIGINT')
253+
const didExit = await waitForExit(child, 5000)
254+
if (didExit) return
255+
child.kill('SIGTERM')
256+
await waitForExit(child, 2000)
257+
}
258+
259+
function waitForExit(child: ChildProcess, timeoutMs: number) {
260+
return Promise.race([
261+
new Promise<boolean>((resolve) => {
262+
child.once('exit', () => resolve(true))
263+
}),
264+
delay(timeoutMs).then(() => false),
265+
])
266+
}
267+
268+
function logAppRunning(getOrigin: () => string) {
269+
console.log(`\n${dim('App running at')} ${bright(getOrigin())}`)
270+
}
271+
272+
function openInBrowser(url: string) {
273+
const os = platform()
274+
if (os === 'darwin') {
275+
spawn('open', [url], { stdio: 'ignore', detached: true }).unref()
276+
return
277+
}
278+
279+
if (os === 'win32') {
280+
spawn('cmd', ['/c', 'start', url], {
281+
stdio: 'ignore',
282+
detached: true,
283+
}).unref()
284+
return
285+
}
286+
287+
spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref()
288+
}
289+
290+
function copyToClipboard(text: string) {
291+
const os = platform()
292+
if (os === 'darwin') {
293+
const proc = spawn('pbcopy', [], { stdio: ['pipe', 'ignore', 'ignore'] })
294+
proc.stdin?.write(text)
295+
proc.stdin?.end()
296+
return
297+
}
298+
299+
if (os === 'win32') {
300+
const proc = spawn('clip', [], { stdio: ['pipe', 'ignore', 'ignore'] })
301+
proc.stdin?.write(text)
302+
proc.stdin?.end()
303+
return
304+
}
305+
306+
const proc = spawn('xclip', ['-selection', 'clipboard'], {
307+
stdio: ['pipe', 'ignore', 'ignore'],
308+
})
309+
proc.stdin?.write(text)
310+
proc.stdin?.end()
311+
}

0 commit comments

Comments
 (0)