phio 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # phio: the pockethost.io CLI
2
+
3
+ **Auth**
4
+
5
+ ```bash
6
+ bunx phio login
7
+ ```
8
+
9
+ **Watch and push local changes instantly**
10
+
11
+ ```bash
12
+ bunx phio dev [instance]
13
+ ```
14
+
15
+ **Bi-directional sync**
16
+
17
+ ```bash
18
+ bunx phio sync [instance]
19
+ ```
20
+
21
+ **Tail logs**
22
+
23
+ ```bash
24
+ bunx phio logs [instance]
25
+ ```
26
+
27
+ ## Configuration
28
+
29
+ Use `pockethost` in your `package.json` to save your instance name so you don't need to keep typing it:
30
+
31
+ ```json
32
+ // package.json
33
+ {
34
+ "pockethost": {
35
+ "instanceId": "all-your-base"
36
+ }
37
+ }
38
+ ```
39
+
40
+ -or-
41
+
42
+ Use `pockethost.json` to save your instance name so you don't need to keep typing it.
43
+
44
+ ```json
45
+ {
46
+ "instanceId": "all-your-base'
47
+ }
48
+ ```
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "phio",
3
+ "version": "0.0.1",
4
+ "description": "A CLI tool to manage your PocketHost instances",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/benallfree/phio"
8
+ },
9
+ "homepage": "https://github.com/benallfree/phio",
10
+ "keywords": [
11
+ "cli",
12
+ "pocketbase",
13
+ "pockethost"
14
+ ],
15
+ "author": "Ben Allfree",
16
+ "license": "MIT",
17
+ "bugs": {
18
+ "url": "https://github.com/benallfree/phio/issues"
19
+ },
20
+ "module": "src/index.ts",
21
+ "type": "module",
22
+ "devDependencies": {
23
+ "@types/bun": "latest",
24
+ "@types/fs-extra": "^11.0.4",
25
+ "@types/node": "^22.5.5"
26
+ },
27
+ "peerDependencies": {
28
+ "typescript": "^5.0.0"
29
+ },
30
+ "scripts": {
31
+ "dev": "tsx --watch ./src/index.ts"
32
+ },
33
+ "bin": {
34
+ "phio": "src/index.ts"
35
+ },
36
+ "files": [
37
+ "src"
38
+ ],
39
+ "dependencies": {
40
+ "@inquirer/prompts": "^5.5.0",
41
+ "@s-libs/micro-dash": "^18.0.0",
42
+ "@samkirkland/ftp-deploy": "^1.2.4",
43
+ "@sentool/fetch-event-source": "^0.5.0",
44
+ "chokidar": "^4.0.0",
45
+ "commander": "^12.1.0",
46
+ "email-validator": "^2.0.4",
47
+ "env-paths": "^3.0.0",
48
+ "env-var": "^7.5.0",
49
+ "event-stream": "^4.0.1",
50
+ "fs-extra": "^11.2.0",
51
+ "ora": "^8.1.0",
52
+ "pocketbase": "^0.21.5"
53
+ }
54
+ }
@@ -0,0 +1,67 @@
1
+ import { Command } from 'commander'
2
+ import { config } from './config'
3
+ import { deploy, excludeDefaults } from '@samkirkland/ftp-deploy'
4
+ import { getClient } from './getClient'
5
+ import { ensureDirSync } from 'fs-extra'
6
+ import { defaultInstanceId } from './defaultInstanceId'
7
+ import { watch } from 'chokidar'
8
+ import { file } from 'bun'
9
+ import { basename, dirname } from 'path'
10
+ import { debounce } from '@s-libs/micro-dash'
11
+
12
+ async function deployMyCode(instanceName: string) {
13
+ const cachePath = '.cache'
14
+ ensureDirSync(cachePath)
15
+
16
+ console.log('🚚 Deploy started')
17
+ await deploy({
18
+ server: 'ftp.pockethost.io',
19
+ username: `__auth__`,
20
+ password: config(`auth`)!.token,
21
+ 'server-dir': `${instanceName}/`,
22
+ exclude: ['*', '!pb_*/**/*'],
23
+ 'state-name': '.cache/.ftp-deploy-sync-state.json',
24
+ 'log-level': 'verbose',
25
+ })
26
+ console.log('🚀 Deploy done!')
27
+ }
28
+
29
+ export const DevCommand = () => {
30
+ return new Command('dev')
31
+ .argument(`[instanceId]`, `Instance name`, defaultInstanceId())
32
+ .description(`Watch for local modifications and sync to remote`)
33
+ .action(async (_instanceId) => {
34
+ if (!_instanceId) {
35
+ console.error(
36
+ 'No instance name provided and none was found in package.json or pockethost.json'
37
+ )
38
+ process.exit(1)
39
+ }
40
+
41
+ const client = getClient()
42
+
43
+ const instance = await client
44
+ .collection(`instances`)
45
+ .getFirstListItem(`id='${_instanceId}' || subdomain='${_instanceId}'`)
46
+
47
+ const upload = debounce(() => {
48
+ deployMyCode(instance.subdomain).catch(console.error)
49
+ }, 200)
50
+
51
+ const watcher = watch(['.'], {
52
+ persistent: true,
53
+ ignored: (file) => {
54
+ const isIgnored = file !== '.' && !file.startsWith('pb_')
55
+ // console.log({ file, isIgnored })
56
+ return isIgnored
57
+ },
58
+ })
59
+ console.log(`Watching for changes in pb_*/**/*`)
60
+ const handle = (path: string, details: any) => {
61
+ upload()
62
+ // internal
63
+ console.log({ path, details })
64
+ }
65
+ watcher.on('add', handle).on('change', handle).on('unlink', handle)
66
+ })
67
+ }
@@ -0,0 +1,59 @@
1
+ import { input, password } from '@inquirer/prompts'
2
+ import { Command } from 'commander'
3
+ import * as EmailValidator from 'email-validator'
4
+ //@ts-ignore
5
+ import { runTasks } from './Task'
6
+ import { config } from './config'
7
+ import { getClient } from './getClient'
8
+
9
+ export const LoginCommand = () =>
10
+ new Command('login')
11
+ .description(`Log in to PocketHost`)
12
+ .helpOption(false)
13
+ .action(async () => {
14
+ while (true) {
15
+ const email = await input({
16
+ message: 'Enter your pockethost.io email address',
17
+ default: config('email'),
18
+ validate: (input: string) => {
19
+ if (!EmailValidator.validate(input)) {
20
+ return 'Invalid email address'
21
+ }
22
+ return true
23
+ },
24
+ })
25
+
26
+ const pw = await password({
27
+ message: 'Enter your pockethost.io password',
28
+ })
29
+
30
+ config(`email`, email)
31
+
32
+ const client = getClient()
33
+ try {
34
+ await runTasks([
35
+ {
36
+ name: `Logging in`,
37
+ run: async () => {
38
+ const res = await client
39
+ .collection('users')
40
+ .authWithPassword(email, pw)
41
+ console.log({ res })
42
+ },
43
+ },
44
+ ])
45
+ } catch (e) {
46
+ console.error(
47
+ `There was an error logging in. Please try again or go to https://pockethost.io to reset your password.`
48
+ )
49
+ continue
50
+ }
51
+
52
+ config(`auth`, {
53
+ token: client.authStore.exportToCookie(),
54
+ record: client.authStore.model,
55
+ })
56
+ break
57
+ }
58
+ console.log(`Logged in!`)
59
+ })
@@ -0,0 +1,77 @@
1
+ import { Command } from 'commander'
2
+ import { config } from './config'
3
+ //@ts-ignore
4
+ import { fetchEventSource } from '@sentool/fetch-event-source'
5
+
6
+ export enum StreamNames {
7
+ StdOut = 'stdout',
8
+ StdErr = 'stderr',
9
+ }
10
+
11
+ export type InstanceLogFields = {
12
+ message: string
13
+ time: string
14
+ stream: StreamNames
15
+ }
16
+ const watchInstanceLog = (
17
+ instanceId: string,
18
+ update: (log: InstanceLogFields) => void,
19
+ nInitial = 100
20
+ ): (() => void) => {
21
+ const controller = new AbortController()
22
+ const signal = controller.signal
23
+ const continuallyFetchFromEventSource = () => {
24
+ const url = `https://${instanceId}.pockethost.io/logs`
25
+ const body = {
26
+ instanceId,
27
+ n: nInitial,
28
+ auth: config(`auth`)!.token,
29
+ }
30
+
31
+ fetchEventSource(url, {
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/json',
35
+ },
36
+ openWhenHidden: true,
37
+ body: JSON.stringify(body),
38
+ onmessage: (event: any) => {
39
+ const { data } = event
40
+
41
+ update(data)
42
+ },
43
+ onopen: async (response: Response) => {
44
+ // console.log(response)
45
+ },
46
+ onerror: (e: Error) => {
47
+ console.error(`got an error`, e)
48
+ },
49
+ onclose: () => {
50
+ console.log(`closed`)
51
+ setTimeout(continuallyFetchFromEventSource, 100)
52
+ },
53
+ signal,
54
+ })
55
+ }
56
+ continuallyFetchFromEventSource()
57
+
58
+ return () => {
59
+ controller.abort()
60
+ }
61
+ }
62
+
63
+ export const LogsCommand = () => {
64
+ return new Command('logs')
65
+ .description(`Tail instance logs`)
66
+ .argument('<instance>', 'Instance ID')
67
+ .action((instance) => {
68
+ watchInstanceLog(instance, (log) => {
69
+ const { time, message, stream } = log
70
+ if (stream === 'stderr') {
71
+ console.error(`[${time}] ${message}`)
72
+ } else {
73
+ console.log(`[${time}] ${message}`)
74
+ }
75
+ })
76
+ })
77
+ }
package/src/Task.ts ADDED
@@ -0,0 +1,19 @@
1
+ import ora from 'ora'
2
+
3
+ interface Task {
4
+ name: string
5
+ run: () => Promise<any>
6
+ }
7
+ export async function runTasks(tasks: Task[]): Promise<void> {
8
+ for (const task of tasks) {
9
+ const spinner = ora(`${task.name}...`).start()
10
+
11
+ try {
12
+ await task.run()
13
+ spinner.succeed(`${task.name}`)
14
+ } catch (error) {
15
+ spinner.fail(`${task.name}`)
16
+ throw error
17
+ }
18
+ }
19
+ }
package/src/config.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { readJSONSync, writeJSONSync } from 'fs-extra/esm'
2
+ import { type AuthModel } from 'pocketbase'
3
+ import { PHIO_HOME } from './constants'
4
+
5
+ export type Config = {
6
+ email: string
7
+ auth: { record: AuthModel; token: string }
8
+ }
9
+ export function config<T extends keyof Config>(
10
+ k: T,
11
+ v?: Config[T]
12
+ ): Config[T] | undefined {
13
+ const configPath = PHIO_HOME('config.json')
14
+ // console.log({ configPath })
15
+ const config = (() => {
16
+ try {
17
+ // console.log(`Reading config`, configPath)
18
+ return readJSONSync(configPath) as Partial<Config>
19
+ } catch (e) {
20
+ // console.warn(`${e}`)
21
+ return {}
22
+ }
23
+ })()
24
+ try {
25
+ if (v !== undefined) {
26
+ config[k] = v
27
+ // console.log(`Writing config`, config, configPath)
28
+ writeJSONSync(configPath, config)
29
+ return v
30
+ }
31
+ return config[k]
32
+ } catch (e) {
33
+ console.error(`${e}`)
34
+ }
35
+ }
@@ -0,0 +1,22 @@
1
+ import envPaths from 'env-paths'
2
+ import env from 'env-var'
3
+ import { ensureDirSync } from 'fs-extra/esm'
4
+ import { join } from 'path'
5
+
6
+ export const PHIO_HOME = (...paths: string[]) =>
7
+ join(
8
+ env.get('PHIO_HOME').default(envPaths(`phio`).config).asString(),
9
+ ...paths
10
+ )
11
+ ensureDirSync(PHIO_HOME())
12
+
13
+ export const PHIO_MOTHERSHIP_URL = (...paths: string[]) => {
14
+ const url = new URL(
15
+ env
16
+ .get(`PHIO_MOTHERSHIP_URL`)
17
+ .default(`https://pockethost-central.pockethost.io`)
18
+ .asString()
19
+ )
20
+ url.pathname = join(url.pathname, ...paths)
21
+ return url.toString()
22
+ }
@@ -0,0 +1,17 @@
1
+ import { existsSync, readFileSync } from 'fs'
2
+
3
+ export const defaultInstanceId = () => {
4
+ if (existsSync('package.json')) {
5
+ const pkg = JSON.parse(readFileSync('package.json').toString())
6
+ if (pkg.pockethost?.instanceId) {
7
+ return pkg.pockethost.instanceId
8
+ }
9
+ }
10
+ if (existsSync('pockethost.json')) {
11
+ const pkg = JSON.parse(readFileSync('pockethost.json').toString())
12
+ if (pkg.instanceId) {
13
+ return pkg.instanceId
14
+ }
15
+ }
16
+ return null
17
+ }
@@ -0,0 +1,17 @@
1
+ import PocketBase from 'pocketbase'
2
+ import { config } from './config'
3
+ import { PHIO_MOTHERSHIP_URL } from './constants'
4
+
5
+ export const getClient = () => {
6
+ const client = new PocketBase(PHIO_MOTHERSHIP_URL())
7
+ const { record, token } = config('auth') || {}
8
+ // console.log({ record, token })
9
+ if (record && token) {
10
+ client.authStore.loadFromCookie(token)
11
+ // console.log({ valid: client.authStore.isValid })
12
+ client.authStore.onChange((token, record) => {
13
+ config('auth', { token, record })
14
+ })
15
+ }
16
+ return client
17
+ }
@@ -0,0 +1,6 @@
1
+ declare module 'fs-extra/esm' {
2
+ function readJSONSync(path: string): any
3
+ function writeJSONSync(path: string, data: any): void
4
+ function ensureDirSync(path: string): void
5
+ export { readJSONSync, writeJSONSync, ensureDirSync }
6
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env tsx
2
+ import { program } from 'commander'
3
+ import { LoginCommand } from './LoginCommand'
4
+ import { version } from '../package.json'
5
+ import { LogsCommand } from './LogsCommand'
6
+ import { DevCommand } from './DevCommand'
7
+
8
+ program
9
+ .name(`PocketHost CLI`)
10
+ .version(version)
11
+ .description(`CLI access to phio`)
12
+ .addCommand(LoginCommand())
13
+ .addCommand(LogsCommand())
14
+ .addCommand(DevCommand())
15
+
16
+ program.parseAsync(process.argv).catch(console.error)