phio 0.2.4 → 0.3.0

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 CHANGED
@@ -39,7 +39,7 @@ Use `pockethost` in your `package.json` to save your instance name so you don't
39
39
  // package.json
40
40
  {
41
41
  "pockethost": {
42
- "instanceId": "all-your-base"
42
+ "instanceName": "all-your-base"
43
43
  }
44
44
  }
45
45
  ```
@@ -50,6 +50,16 @@ Use `pockethost.json` to save your instance name so you don't need to keep typin
50
50
 
51
51
  ```json
52
52
  {
53
- "instanceId": "all-your-base'
53
+ "instanceName": "all-your-base"
54
54
  }
55
55
  ```
56
+
57
+ ## Environment Variables
58
+
59
+ The following environment variables can be used to override any saved configuration:
60
+
61
+ - `PHIO_USERNAME` - Override saved username
62
+ - `PHIO_PASSWORD` - Override saved password
63
+ - `PHIO_INSTANCE_NAME` - Override saved instance name
64
+
65
+ Environment variables take precedence over configuration in package.json or pockethost.json.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phio",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "A CLI tool to manage your PocketHost instances",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,11 +28,8 @@
28
28
  "@types/node": "^22.5.5",
29
29
  "prettier-plugin-organize-imports": "^4.0.0"
30
30
  },
31
- "peerDependencies": {
32
- "typescript": "^5.6.2"
33
- },
34
31
  "scripts": {
35
- "dev": "tsx --watch ./src/index.ts"
32
+ "dev": "tsx ./src/cli.ts"
36
33
  },
37
34
  "bin": {
38
35
  "phio": "src/cli.ts"
@@ -56,7 +53,8 @@
56
53
  "multimatch": "^7.0.0",
57
54
  "ora": "^8.1.0",
58
55
  "pocketbase": "^0.21.5",
59
- "tsx": "^4.19.1"
56
+ "tsx": "^4.19.1",
57
+ "typescript": "^5.6.2"
60
58
  },
61
59
  "prettier": {
62
60
  "semi": false,
package/src/cli.ts CHANGED
@@ -3,6 +3,7 @@ import { program } from 'commander'
3
3
  import { version } from '../package.json'
4
4
  import { DeployCommand } from './commands/DeployCommand'
5
5
  import { DevCommand } from './commands/DevCommand'
6
+ import { InfoCommand } from './commands/InfoCommand'
6
7
  import { LinkCommand } from './commands/LinkCommand'
7
8
  import { ListCommand } from './commands/ListCommand'
8
9
  import { LoginCommand } from './commands/LoginCommand'
@@ -20,5 +21,19 @@ program
20
21
  .addCommand(ListCommand())
21
22
  .addCommand(LinkCommand())
22
23
  .addCommand(DeployCommand())
24
+ .addCommand(InfoCommand())
23
25
 
24
- program.parseAsync(process.argv).catch(console.error)
26
+ // Add error handling
27
+ program.exitOverride()
28
+
29
+ program.parseAsync(process.argv).catch((err) => {
30
+ // Handle specific commander error types
31
+ if (err.code === 'commander.unknownCommand') {
32
+ console.error('Error: Unknown command')
33
+ } else if (err.code === 'commander.missingArgument') {
34
+ console.error('Error: Missing required argument')
35
+ } else {
36
+ console.error('Error:', err.message)
37
+ }
38
+ process.exit(1)
39
+ })
@@ -1,10 +1,10 @@
1
1
  import { Command } from 'commander'
2
- import { savedInstanceId } from '../lib/defaultInstanceId'
2
+ import { savedInstanceName } from '../lib/defaultInstanceId'
3
3
  import { DEFAULT_EXCLUDES, DEFAULT_INCLUDES, deployMyCode } from './DevCommand'
4
4
 
5
5
  export const DeployCommand = () => {
6
6
  return new Command(`deploy`)
7
- .argument(`[instanceId]`, `Instance name`, savedInstanceId())
7
+ .argument(`[instanceName]`, `Instance name`, savedInstanceName())
8
8
  .description(`Deploy to remote`)
9
9
  .option(`-v, --verbose`, `Verbose output`)
10
10
  .option(
@@ -21,8 +21,8 @@ export const DeployCommand = () => {
21
21
  },
22
22
  DEFAULT_EXCLUDES
23
23
  )
24
- .action((instanceId, options) => {
24
+ .action((instanceName, options) => {
25
25
  const { include, exclude, verbose } = options
26
- deployMyCode(instanceId, include, exclude, verbose)
26
+ deployMyCode(instanceName, include, exclude, verbose)
27
27
  })
28
28
  }
@@ -4,11 +4,10 @@ import { IFtpDeployArguments } from '@samkirkland/ftp-deploy/src/types'
4
4
  import Bottleneck from 'bottleneck'
5
5
  import { watch } from 'chokidar'
6
6
  import { Command } from 'commander'
7
- import { ensureDirSync } from 'fs-extra'
8
7
  import multimatch from 'multimatch'
9
8
  import { config } from '../lib/config'
10
9
  import { getInstanceBySubdomainCnameOrId } from '../lib/getClient'
11
- import { savedInstanceId } from './../lib/defaultInstanceId'
10
+ import { savedInstanceName } from './../lib/defaultInstanceId'
12
11
 
13
12
  export const DEFAULT_INCLUDES = [
14
13
  `pb_*`,
@@ -93,10 +92,7 @@ export async function deployMyCode(
93
92
  exclude: string[],
94
93
  verbose: boolean
95
94
  ) {
96
- const cachePath = '.cache'
97
- ensureDirSync(cachePath)
98
-
99
- console.log('🚚 Deploy started')
95
+ console.log(`🚚 Deploy started for ${instanceName}`)
100
96
  const args: IFtpDeployArguments = {
101
97
  server: 'ftp.pockethost.io',
102
98
  username: `__auth__`,
@@ -104,18 +100,16 @@ export async function deployMyCode(
104
100
  'server-dir': `${instanceName}/`,
105
101
  include,
106
102
  exclude: [...excludeDefaults, ...exclude],
107
- 'log-level': verbose ? 'verbose' : 'standard',
103
+ 'log-level': verbose ? 'verbose' : 'minimal',
108
104
  }
109
105
 
110
- console.log({ args })
111
-
112
106
  await deploy(args)
113
107
  console.log('🚀 Deploy done!')
114
108
  }
115
109
 
116
110
  export const DevCommand = () => {
117
111
  return new Command('dev')
118
- .argument(`[instanceId]`, `Instance name`, savedInstanceId())
112
+ .argument(`[instanceId]`, `Instance name`, savedInstanceName())
119
113
  .description(`Watch for local modifications and sync to remote`)
120
114
  .option(`-v, --verbose`, `Verbose output`)
121
115
  .option(
@@ -0,0 +1,10 @@
1
+ import { Command } from 'commander'
2
+ import { PHIO_HOME } from '../lib/constants'
3
+ import { savedInstanceName } from '../lib/defaultInstanceId'
4
+
5
+ export const InfoCommand = () => {
6
+ return new Command(`info`).description(`Get config info`).action(() => {
7
+ console.log(`Config root: ${PHIO_HOME()}`)
8
+ console.log(`Instance: ${savedInstanceName()}`)
9
+ })
10
+ }
@@ -1,38 +1,44 @@
1
1
  import { select } from '@inquirer/prompts'
2
2
  import { Command } from 'commander'
3
- import { config } from '../lib/config'
4
- import { saveInstanceId } from '../lib/defaultInstanceId'
3
+ import { saveInstanceName } from '../lib/defaultInstanceId'
5
4
  import { InstanceFields } from '../lib/InstanceFields'
6
5
  import { getClient, getInstanceBySubdomainCnameOrId } from './../lib/getClient'
7
6
 
8
- export const isLinked = () => !!config('instanceId')
9
-
10
- export const link = async (instanceNameOrId: string) => {
11
- saveInstanceId(instanceNameOrId, 'package.json')
12
- const instance = await getInstanceBySubdomainCnameOrId(instanceNameOrId)
7
+ export const link = async (instanceName: string) => {
8
+ saveInstanceName(instanceName, 'package.json')
9
+ const instance = await getInstanceBySubdomainCnameOrId(instanceName)
13
10
  if (!instance) {
14
11
  return
15
12
  }
16
- config('instanceId', instance.subdomain)
17
13
  return instance
18
14
  }
19
15
 
20
16
  export const linkWithUserInput = async () => {
21
- const client = getClient()
17
+ const client = await getClient()
22
18
  const instances = await client
23
19
  .collection(`instances`)
24
20
  .getFullList<InstanceFields>()
21
+
22
+ if (instances.length === 0) {
23
+ console.error(
24
+ `No instances found. If this seems wrong, use 'phio login' to log in, then try again.`
25
+ )
26
+ return
27
+ }
28
+
25
29
  while (true) {
26
- const instanceNameOrId = await select({
30
+ const instanceName = await select({
27
31
  message: `Choose the instance you'd like to link`,
28
- choices: instances.map((instance) => ({
29
- name: `${instance.subdomain} (${instance.id}) ${
30
- instance.cname ? `(${instance.cname})` : ''
31
- } (${instance.status.toUpperCase()})`,
32
- value: instance.subdomain,
33
- })),
32
+ choices: instances
33
+ .sort((a, b) => a.subdomain.localeCompare(b.subdomain))
34
+ .map((instance) => ({
35
+ name: `${instance.subdomain} (${instance.id}) ${
36
+ instance.cname ? `(${instance.cname})` : ''
37
+ } (${instance.status.toUpperCase()})`,
38
+ value: instance.subdomain,
39
+ })),
34
40
  })
35
- const instance = await link(instanceNameOrId)
41
+ const instance = await link(instanceName)
36
42
  if (!instance) {
37
43
  console.error(`Instance not found`)
38
44
  continue
@@ -4,18 +4,21 @@ import { InstanceFields } from './../lib/InstanceFields'
4
4
 
5
5
  export const ListCommand = () => {
6
6
  return new Command(`list`)
7
+ .alias(`ls`)
7
8
  .description(`List all the logs`)
8
9
  .action(async () => {
9
- const client = getClient()
10
+ const client = await getClient()
10
11
  const instances = await client
11
12
  .collection(`instances`)
12
13
  .getFullList<InstanceFields>()
13
- instances.forEach((instance) => {
14
- console.log(
15
- `- ${instance.subdomain} (${instance.id}) ${
16
- instance.cname ? `(${instance.cname})` : ''
17
- } (${instance.status.toUpperCase()})`
18
- )
19
- })
14
+ instances
15
+ .sort((a, b) => a.subdomain.localeCompare(b.subdomain))
16
+ .forEach((instance) => {
17
+ console.log(
18
+ `- ${instance.subdomain} (${instance.id}) ${
19
+ instance.cname ? `(${instance.cname})` : ''
20
+ } (${instance.status.toUpperCase()})`
21
+ )
22
+ })
20
23
  })
21
24
  }
@@ -2,10 +2,16 @@ import { input, password } from '@inquirer/prompts'
2
2
  import { Command } from 'commander'
3
3
  import * as EmailValidator from 'email-validator'
4
4
  import { config } from '../lib/config'
5
- import { getClient } from './../lib/getClient'
6
- import { runTasks } from './../lib/Task'
5
+ import { PHIO_USERNAME } from '../lib/constants'
6
+ import { login } from './../lib/getClient'
7
7
 
8
8
  export const loginWithUserInput = async () => {
9
+ if (PHIO_USERNAME()) {
10
+ throw new Error(
11
+ 'Cannot login with username and password if PHIO_USERNAME is set'
12
+ )
13
+ }
14
+
9
15
  while (true) {
10
16
  const email = await input({
11
17
  message: 'Enter your pockethost.io email address',
@@ -24,29 +30,20 @@ export const loginWithUserInput = async () => {
24
30
 
25
31
  config(`email`, email)
26
32
 
27
- const client = getClient()
28
33
  try {
29
- await runTasks([
30
- {
31
- name: `Logging in`,
32
- run: async () => {
33
- const res = await client
34
- .collection('users')
35
- .authWithPassword(email, pw)
36
- },
37
- },
38
- ])
34
+ const authStore = await login(email, pw)
35
+
36
+ config(`auth`, {
37
+ token: authStore.exportToCookie(),
38
+ record: authStore.model,
39
+ })
39
40
  } catch (e) {
40
41
  console.error(
41
- `There was an error logging in. Please try again or go to https://pockethost.io to reset your password.`
42
+ `There was an error logging in. Please try again or go to https://pockethost.io to reset your password. (${e})`
42
43
  )
43
44
  continue
44
45
  }
45
46
 
46
- config(`auth`, {
47
- token: client.authStore.exportToCookie(),
48
- record: client.authStore.model,
49
- })
50
47
  break
51
48
  }
52
49
  console.log(`Logged in!`)
@@ -1,7 +1,7 @@
1
1
  import { fetchEventSource } from '@sentool/fetch-event-source'
2
2
  import { Command } from 'commander'
3
3
  import { config } from '../lib/config'
4
- import { savedInstanceId } from '../lib/defaultInstanceId'
4
+ import { savedInstanceName } from '../lib/defaultInstanceId'
5
5
  import { ensureLoggedIn } from '../lib/ensureLoggedIn'
6
6
 
7
7
  export enum StreamNames {
@@ -70,7 +70,7 @@ const watchInstanceLog = async (
70
70
  export const LogsCommand = () => {
71
71
  return new Command('logs')
72
72
  .description(`Tail instance logs`)
73
- .argument('[instance]', 'Instance ID', savedInstanceId())
73
+ .argument('[instance]', 'Instance ID', savedInstanceName())
74
74
  .action((instance) => {
75
75
  watchInstanceLog(instance, (log) => {
76
76
  const { time, message, stream } = log
package/src/lib/config.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { readJSONSync, writeJSONSync } from 'fs-extra/esm'
1
+ import fse from 'fs-extra'
2
2
  import { type AuthModel } from 'pocketbase'
3
3
  import { PHIO_HOME } from './constants'
4
4
 
5
+ const { readJSONSync, writeJSONSync } = fse
5
6
  export type Config = {
6
- instanceId: string
7
7
  email: string
8
8
  auth: { record: AuthModel; token: string }
9
9
  }
@@ -1,8 +1,10 @@
1
1
  import envPaths from 'env-paths'
2
2
  import env from 'env-var'
3
- import { ensureDirSync } from 'fs-extra/esm'
3
+ import fse from 'fs-extra'
4
4
  import { join } from 'path'
5
5
 
6
+ const { ensureDirSync } = fse
7
+
6
8
  export const PHIO_HOME = (...paths: string[]) =>
7
9
  join(
8
10
  env.get('PHIO_HOME').default(envPaths(`phio`).config).asString(),
@@ -20,3 +22,8 @@ export const PHIO_MOTHERSHIP_URL = (...paths: string[]) => {
20
22
  url.pathname = join(url.pathname, ...paths)
21
23
  return url.toString()
22
24
  }
25
+
26
+ export const PHIO_USERNAME = () => env.get('PHIO_USERNAME').asString() || ''
27
+ export const PHIO_PASSWORD = () => env.get('PHIO_PASSWORD').asString() || ''
28
+ export const PHIO_INSTANCE_NAME = () =>
29
+ env.get('PHIO_INSTANCE_NAME').asString() || ''
@@ -1,29 +1,35 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'fs'
2
+ import { PHIO_INSTANCE_NAME } from './constants'
2
3
 
3
- export const savedInstanceId = () => {
4
+ export const savedInstanceName = () => {
5
+ if (PHIO_INSTANCE_NAME()) {
6
+ return PHIO_INSTANCE_NAME()
7
+ }
4
8
  if (existsSync('package.json')) {
5
9
  const pkg = JSON.parse(readFileSync('package.json').toString())
6
- if (pkg.pockethost?.instanceId) {
7
- return pkg.pockethost.instanceId
10
+ if (pkg.pockethost?.instanceName) {
11
+ return pkg.pockethost.instanceName
8
12
  }
9
13
  }
10
14
  if (existsSync('pockethost.json')) {
11
15
  const pkg = JSON.parse(readFileSync('pockethost.json').toString())
12
- if (pkg.instanceId) {
13
- return pkg.instanceId
16
+ if (pkg.instanceName) {
17
+ return pkg.instanceName
14
18
  }
15
19
  }
16
20
  return null
17
21
  }
18
22
 
19
- export const saveInstanceId = (
20
- instanceId: string,
23
+ export const saveInstanceName = (
24
+ instanceName: string,
21
25
  file: 'package.json' | 'pockethost.json'
22
26
  ) => {
23
27
  if (!existsSync(file)) {
24
28
  // Create new file if it doesn't exist
25
29
  const newContent =
26
- file === 'package.json' ? { pockethost: { instanceId } } : { instanceId }
30
+ file === 'package.json'
31
+ ? { pockethost: { instanceName } }
32
+ : { instanceName }
27
33
  writeFileSync(file, JSON.stringify(newContent, null, 2))
28
34
  return
29
35
  }
@@ -33,9 +39,9 @@ export const saveInstanceId = (
33
39
 
34
40
  if (file === 'package.json') {
35
41
  content.pockethost = content.pockethost || {}
36
- content.pockethost.instanceId = instanceId
42
+ content.pockethost.instanceName = instanceName
37
43
  } else {
38
- content.instanceId = instanceId
44
+ content.instanceName = instanceName
39
45
  }
40
46
 
41
47
  writeFileSync(file, JSON.stringify(content, null, 2))
@@ -1,18 +1,8 @@
1
- import { config } from '../lib/config'
2
1
  import { getClient } from '../lib/getClient'
3
2
 
4
3
  export const ensureLoggedIn = async () => {
5
- try {
6
- const token = config(`auth`)!.token
7
- const client = getClient()
8
- client.authStore.loadFromCookie(token)
9
- await client.collection(`users`).authRefresh()
10
- config(`auth`, {
11
- token: client.authStore.exportToCookie(),
12
- record: client.authStore.model,
13
- })
14
- } catch (e) {
15
- console.error(`You must be logged in first. Use 'phio login'`)
16
- process.exit(1)
4
+ const client = await getClient()
5
+ if (!client.authStore.isValid) {
6
+ throw new Error(`You must be logged in first. Use 'phio login'`)
17
7
  }
18
8
  }
@@ -1,26 +1,81 @@
1
1
  import PocketBase from 'pocketbase'
2
+ import { runTasks } from './../lib/Task'
2
3
  import { config } from './config'
3
- import { PHIO_MOTHERSHIP_URL } from './constants'
4
+ import { PHIO_MOTHERSHIP_URL, PHIO_PASSWORD, PHIO_USERNAME } from './constants'
4
5
 
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) {
6
+ let client: PocketBase | undefined
7
+ export const getClient = async () => {
8
+ if (client) {
9
+ return client
10
+ }
11
+ client = new PocketBase(PHIO_MOTHERSHIP_URL())
12
+
13
+ if (PHIO_USERNAME()) {
14
+ try {
15
+ await unsafeLogin(PHIO_USERNAME(), PHIO_PASSWORD())
16
+ return client
17
+ } catch (e) {
18
+ throw new Error(
19
+ `There was an error logging in. Please try again or go to https://pockethost.io to reset your password.`
20
+ )
21
+ }
22
+ }
23
+
24
+ const authStore = config('auth')
25
+ if (authStore) {
26
+ const { record, token } = authStore
10
27
  client.authStore.loadFromCookie(token)
11
28
  // console.log({ valid: client.authStore.isValid })
12
29
  client.authStore.onChange((token, record) => {
13
- config('auth', { token: client.authStore.exportToCookie(), record })
30
+ if (!client) {
31
+ console.warn('No client found - please report this bug')
32
+ return
33
+ }
34
+ config('auth', {
35
+ token: client.authStore.exportToCookie(),
36
+ record: client.authStore.model,
37
+ })
38
+ })
39
+ await client.collection(`users`).authRefresh()
40
+ config(`auth`, {
41
+ token: client.authStore.exportToCookie(),
42
+ record: client.authStore.model,
14
43
  })
15
44
  }
16
45
  return client
17
46
  }
18
47
 
19
48
  export const getInstanceBySubdomainCnameOrId = async (search: string) => {
20
- const client = getClient()
49
+ const client = await getClient()
21
50
  return await client
22
51
  .collection(`instances`)
23
52
  .getFirstListItem(
24
53
  `id='${search}' || subdomain='${search}' || cname='${search}'`
25
54
  )
26
55
  }
56
+
57
+ const unsafeLogin = async (username: string, password: string) => {
58
+ if (!client) {
59
+ throw new Error('No client found')
60
+ }
61
+ await runTasks([
62
+ {
63
+ name: `Logging in`,
64
+ run: async () => {
65
+ if (!client) {
66
+ throw new Error('No client found')
67
+ }
68
+ const res = await client
69
+ .collection('users')
70
+ .authWithPassword(username, password)
71
+ },
72
+ },
73
+ ])
74
+ return client.authStore
75
+ }
76
+
77
+ export const login = async (username: string, password: string) => {
78
+ const client = await getClient()
79
+ await unsafeLogin(username, password)
80
+ return client.authStore
81
+ }