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 +48 -0
- package/package.json +54 -0
- package/src/DevCommand.ts +67 -0
- package/src/LoginCommand.ts +59 -0
- package/src/LogsCommand.ts +77 -0
- package/src/Task.ts +19 -0
- package/src/config.ts +35 -0
- package/src/constants.ts +22 -0
- package/src/defaultInstanceId.ts +17 -0
- package/src/getClient.ts +17 -0
- package/src/global.d.ts +6 -0
- package/src/index.ts +16 -0
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
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -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
|
+
}
|
package/src/getClient.ts
ADDED
|
@@ -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
|
+
}
|
package/src/global.d.ts
ADDED
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)
|