phio 0.1.1 → 0.2.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
@@ -19,10 +19,10 @@ bunx phio list
19
19
  bunx phio dev [instance]
20
20
  ```
21
21
 
22
- **Bi-directional sync**
22
+ **Deploy to remote**
23
23
 
24
24
  ```bash
25
- bunx phio sync [instance]
25
+ bunx phio deploy [instance]
26
26
  ```
27
27
 
28
28
  **Tail logs**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phio",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "A CLI tool to manage your PocketHost instances",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,8 +17,10 @@
17
17
  "bugs": {
18
18
  "url": "https://github.com/pockethost/phio/issues"
19
19
  },
20
+ "main": "src/index.ts",
20
21
  "module": "src/index.ts",
21
22
  "type": "module",
23
+ "types": "src/index.ts",
22
24
  "devDependencies": {
23
25
  "@types/bun": "latest",
24
26
  "@types/fs-extra": "^11.0.4",
@@ -32,7 +34,7 @@
32
34
  "dev": "tsx --watch ./src/index.ts"
33
35
  },
34
36
  "bin": {
35
- "phio": "src/index.ts"
37
+ "phio": "src/cli.ts"
36
38
  },
37
39
  "files": [
38
40
  "src"
package/src/cli.ts ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env tsx
2
+ import { program } from 'commander'
3
+ import { version } from '../package.json'
4
+ import { DeployCommand } from './commands/DeployCommand'
5
+ import { DevCommand } from './commands/DevCommand'
6
+ import { LinkCommand } from './commands/LinkCommand'
7
+ import { ListCommand } from './commands/ListCommand'
8
+ import { LoginCommand } from './commands/LoginCommand'
9
+ import { LogsCommand } from './commands/LogsCommand'
10
+ import { WhoAmICommand } from './commands/WhoAmICommand'
11
+
12
+ program
13
+ .name(`PocketHost CLI`)
14
+ .version(version)
15
+ .description(`CLI access to phio`)
16
+ .addCommand(LoginCommand())
17
+ .addCommand(LogsCommand())
18
+ .addCommand(DevCommand())
19
+ .addCommand(WhoAmICommand())
20
+ .addCommand(ListCommand())
21
+ .addCommand(LinkCommand())
22
+ .addCommand(DeployCommand())
23
+
24
+ program.parseAsync(process.argv).catch(console.error)
@@ -0,0 +1,26 @@
1
+ import { Command } from 'commander'
2
+ import { savedInstanceId } from '../lib/defaultInstanceId'
3
+ import { DEFAULT_EXCLUDES, DEFAULT_INCLUDES, deployMyCode } from './DevCommand'
4
+
5
+ export const DeployCommand = () => {
6
+ return new Command(`deploy`)
7
+ .argument(`[instanceId]`, `Instance name`, savedInstanceId())
8
+ .description(`Deploy to remote`)
9
+ .option(`-v, --verbose`, `Verbose output`)
10
+ .option(
11
+ '-i, --include <include...>',
12
+ 'Files to include in the sync',
13
+ (val, prev) => [...prev, val],
14
+ DEFAULT_INCLUDES
15
+ )
16
+ .option(
17
+ '-e, --exclude <exclude...>',
18
+ 'Files to exclude from the sync',
19
+ (val, prev) => [...prev, val],
20
+ DEFAULT_EXCLUDES
21
+ )
22
+ .action((instanceId, options) => {
23
+ const { include, exclude, verbose } = options
24
+ deployMyCode(instanceId, include, exclude, verbose)
25
+ })
26
+ }
@@ -7,12 +7,90 @@ import { ensureDirSync } from 'fs-extra'
7
7
  import multimatch from 'multimatch'
8
8
  import { config } from '../lib/config'
9
9
  import { getInstanceBySubdomainCnameOrId } from '../lib/getClient'
10
- import { defaultInstanceId } from './../lib/defaultInstanceId'
10
+ import { savedInstanceId } from './../lib/defaultInstanceId'
11
11
 
12
- async function deployMyCode(
12
+ export const DEFAULT_INCLUDES = [
13
+ `pb_*`,
14
+ 'pb_*/**/*',
15
+ `package.json`,
16
+ `bun.lockb`,
17
+ `patches`,
18
+ `patches/**/*`,
19
+ ]
20
+
21
+ export const DEFAULT_EXCLUDES = [`pb_data`, `pb_data/**/*`]
22
+
23
+ export type DeployOptions = {
24
+ include: string[]
25
+ exclude: string[]
26
+ verbose: boolean
27
+ }
28
+
29
+ export const watchAndDeploy = async (
30
+ _instanceId: string,
31
+ options: DeployOptions = {
32
+ include: DEFAULT_INCLUDES,
33
+ exclude: DEFAULT_EXCLUDES,
34
+ verbose: false,
35
+ }
36
+ ) => {
37
+ if (!_instanceId) {
38
+ console.error(
39
+ `No instance name provided and none was found in package.json or pockethost.json. Use 'phio link <instance>'`
40
+ )
41
+ process.exit(1)
42
+ }
43
+ console.log(`Dev mode`)
44
+ const { include, exclude, verbose } = options
45
+ // console.log({ include, exclude })
46
+
47
+ const instance = await getInstanceBySubdomainCnameOrId(_instanceId)
48
+
49
+ const limiter = new Bottleneck({ maxConcurrent: 1 })
50
+ const upload = debounce(
51
+ () =>
52
+ limiter.schedule(() =>
53
+ deployMyCode(instance.subdomain, include, exclude, verbose).catch(
54
+ console.error
55
+ )
56
+ ),
57
+ 200
58
+ )
59
+
60
+ const watcher = watch('.', {
61
+ persistent: true,
62
+ ignored: (file) => {
63
+ if (file === '.') return false
64
+ const isIncluded = multimatch([file], include).length > 0
65
+ const isExcluded = multimatch([file], exclude).length > 0
66
+ const isIgnored = !isIncluded || isExcluded
67
+ // console.log({
68
+ // file,
69
+ // include,
70
+ // isIncluded,
71
+ // exclude,
72
+ // isExcluded,
73
+ // isIgnored,
74
+ // })
75
+ return isIgnored
76
+ },
77
+ })
78
+ console.log(
79
+ `Watching for changes in ${include.join(', ')} and excluding ${exclude.join(', ')}`
80
+ )
81
+ const handle = (path: string, details: any) => {
82
+ upload()
83
+ // internal
84
+ // console.log(`Syncing ${path}`)
85
+ }
86
+ watcher.on('add', handle).on('change', handle).on('unlink', handle)
87
+ }
88
+
89
+ export async function deployMyCode(
13
90
  instanceName: string,
14
91
  include: string[],
15
- exclude: string[]
92
+ exclude: string[],
93
+ verbose: boolean
16
94
  ) {
17
95
  const cachePath = '.cache'
18
96
  ensureDirSync(cachePath)
@@ -25,84 +103,27 @@ async function deployMyCode(
25
103
  'server-dir': `${instanceName}/`,
26
104
  include,
27
105
  exclude: [...excludeDefaults, ...exclude],
28
- 'state-name': '.cache/.ftp-deploy-sync-state.json',
29
- 'log-level': 'verbose',
106
+ 'log-level': verbose ? 'verbose' : 'standard',
30
107
  })
31
108
  console.log('🚀 Deploy done!')
32
109
  }
33
110
 
34
111
  export const DevCommand = () => {
35
112
  return new Command('dev')
36
- .argument(`[instanceId]`, `Instance name`, defaultInstanceId())
113
+ .argument(`[instanceId]`, `Instance name`, savedInstanceId())
37
114
  .description(`Watch for local modifications and sync to remote`)
115
+ .option(`-v, --verbose`, `Verbose output`)
38
116
  .option(
39
117
  '-i, --include <include...>',
40
118
  'Files to include in the sync',
41
119
  (val, prev) => [...prev, val],
42
- [
43
- `pb_*`,
44
- 'pb_*/**/*',
45
- `package.json`,
46
- `bun.lockb`,
47
- `patches`,
48
- `patches/**/*`,
49
- ]
120
+ DEFAULT_INCLUDES
50
121
  )
51
122
  .option(
52
123
  '-e, --exclude <exclude...>',
53
124
  'Files to exclude from the sync',
54
125
  (val, prev) => [...prev, val],
55
- [`pb_data`, `pb_data/**/*`]
126
+ DEFAULT_EXCLUDES
56
127
  )
57
- .action(async (_instanceId, { include, exclude }) => {
58
- if (!_instanceId) {
59
- console.error(
60
- `No instance name provided and none was found in package.json or pockethost.json. Use 'phio link <instance>'`
61
- )
62
- process.exit(1)
63
- }
64
- console.log(`Dev mode`)
65
- console.log({ include, exclude })
66
-
67
- const instance = await getInstanceBySubdomainCnameOrId(_instanceId)
68
-
69
- const limiter = new Bottleneck({ maxConcurrent: 1 })
70
- const upload = debounce(
71
- () =>
72
- limiter.schedule(() =>
73
- deployMyCode(instance.subdomain, include, exclude).catch(
74
- console.error
75
- )
76
- ),
77
- 200
78
- )
79
-
80
- const watcher = watch('.', {
81
- persistent: true,
82
- ignored: (file) => {
83
- if (file === '.') return false
84
- const isIncluded = multimatch([file], include).length > 0
85
- const isExcluded = multimatch([file], exclude).length > 0
86
- const isIgnored = !isIncluded || isExcluded
87
- console.log({
88
- file,
89
- include,
90
- isIncluded,
91
- exclude,
92
- isExcluded,
93
- isIgnored,
94
- })
95
- return isIgnored
96
- },
97
- })
98
- console.log(
99
- `Watching for changes in ${include.join(', ')} and excluding ${exclude.join(', ')}`
100
- )
101
- const handle = (path: string, details: any) => {
102
- upload()
103
- // internal
104
- console.log(`Syncing ${path}`)
105
- }
106
- watcher.on('add', handle).on('change', handle).on('unlink', handle)
107
- })
128
+ .action(watchAndDeploy)
108
129
  }
@@ -1,21 +1,59 @@
1
+ import { select } from '@inquirer/prompts'
1
2
  import { Command } from 'commander'
2
- import { readJSONSync, writeJSONSync } from 'fs-extra/esm'
3
- import { getInstanceBySubdomainCnameOrId } from './../lib/getClient'
3
+ import { config } from '../lib/config'
4
+ import { InstanceFields } from '../lib/InstanceFields'
5
+ import { getClient, getInstanceBySubdomainCnameOrId } from './../lib/getClient'
6
+
7
+ export const isLinked = () => !!config('instanceId')
8
+
9
+ export const link = async (instanceNameOrId: string) => {
10
+ const instance = await getInstanceBySubdomainCnameOrId(instanceNameOrId)
11
+ if (!instance) {
12
+ return
13
+ }
14
+ config('instanceId', instance.subdomain)
15
+ return instance
16
+ }
17
+
18
+ export const linkWithUserInput = async () => {
19
+ const client = getClient()
20
+ const instances = await client
21
+ .collection(`instances`)
22
+ .getFullList<InstanceFields>()
23
+ while (true) {
24
+ const instanceNameOrId = await select({
25
+ message: `Choose the instance you'd like to link`,
26
+ choices: instances.map((instance) => ({
27
+ name: `${instance.subdomain} (${instance.id}) ${
28
+ instance.cname ? `(${instance.cname})` : ''
29
+ } (${instance.status.toUpperCase()})`,
30
+ value: instance.subdomain,
31
+ })),
32
+ })
33
+ const instance = await link(instanceNameOrId)
34
+ if (!instance) {
35
+ console.error(`Instance not found`)
36
+ continue
37
+ }
38
+ console.log(`Linked ${instance.subdomain}`)
39
+ break
40
+ }
41
+ }
4
42
 
5
43
  export const LinkCommand = () => {
6
44
  return new Command(`link`)
7
- .argument(`<instance>`, `Instance name or ID`)
45
+ .argument(`[instance]`, `Instance name or ID`)
8
46
  .description(`Link a local directory to a remote instance`)
9
47
  .action(async (_anyName) => {
10
- const instance = await getInstanceBySubdomainCnameOrId(_anyName)
11
- if (!instance) {
12
- console.error(`Instance not found`)
13
- process.exit(1)
48
+ if (_anyName) {
49
+ const instance = await link(_anyName)
50
+ if (!instance) {
51
+ console.error(`Instance ${_anyName} not found`)
52
+ process.exit(1)
53
+ }
54
+ console.log(`Linked ${instance.subdomain}`)
55
+ return
14
56
  }
15
- console.log(`Instance found: ${instance.subdomain}`)
16
- const pkg = readJSONSync(`package.json`)
17
- pkg.pockethost = { instanceId: instance.subdomain }
18
- console.log(`Writing to package.json`)
19
- writeJSONSync(`package.json`, pkg, { spaces: 2 })
57
+ await linkWithUserInput()
20
58
  })
21
59
  }
@@ -5,53 +5,55 @@ import { config } from '../lib/config'
5
5
  import { getClient } from './../lib/getClient'
6
6
  import { runTasks } from './../lib/Task'
7
7
 
8
- export const LoginCommand = () =>
9
- new Command('login')
10
- .description(`Log in to PocketHost`)
11
- .helpOption(false)
12
- .action(async () => {
13
- while (true) {
14
- const email = await input({
15
- message: 'Enter your pockethost.io email address',
16
- default: config('email'),
17
- validate: (input: string) => {
18
- if (!EmailValidator.validate(input)) {
19
- return 'Invalid email address'
20
- }
21
- return true
22
- },
23
- })
8
+ export const loginWithUserInput = async () => {
9
+ while (true) {
10
+ const email = await input({
11
+ message: 'Enter your pockethost.io email address',
12
+ default: config('email'),
13
+ validate: (input: string) => {
14
+ if (!EmailValidator.validate(input)) {
15
+ return 'Invalid email address'
16
+ }
17
+ return true
18
+ },
19
+ })
24
20
 
25
- const pw = await password({
26
- message: 'Enter your pockethost.io password',
27
- })
21
+ const pw = await password({
22
+ message: 'Enter your pockethost.io password',
23
+ })
28
24
 
29
- config(`email`, email)
25
+ config(`email`, email)
30
26
 
31
- const client = getClient()
32
- try {
33
- await runTasks([
34
- {
35
- name: `Logging in`,
36
- run: async () => {
37
- const res = await client
38
- .collection('users')
39
- .authWithPassword(email, pw)
40
- },
41
- },
42
- ])
43
- } catch (e) {
44
- console.error(
45
- `There was an error logging in. Please try again or go to https://pockethost.io to reset your password.`
46
- )
47
- continue
48
- }
27
+ const client = getClient()
28
+ 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
+ ])
39
+ } catch (e) {
40
+ console.error(
41
+ `There was an error logging in. Please try again or go to https://pockethost.io to reset your password.`
42
+ )
43
+ continue
44
+ }
49
45
 
50
- config(`auth`, {
51
- token: client.authStore.exportToCookie(),
52
- record: client.authStore.model,
53
- })
54
- break
55
- }
56
- console.log(`Logged in!`)
46
+ config(`auth`, {
47
+ token: client.authStore.exportToCookie(),
48
+ record: client.authStore.model,
57
49
  })
50
+ break
51
+ }
52
+ console.log(`Logged in!`)
53
+ }
54
+
55
+ export const LoginCommand = () =>
56
+ new Command('login')
57
+ .description(`Log in to PocketHost`)
58
+ .helpOption(false)
59
+ .action(loginWithUserInput)
@@ -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 { defaultInstanceId } from '../lib/defaultInstanceId'
4
+ import { savedInstanceId } from '../lib/defaultInstanceId'
5
5
 
6
6
  export enum StreamNames {
7
7
  StdOut = 'stdout',
@@ -64,7 +64,7 @@ const watchInstanceLog = (
64
64
  export const LogsCommand = () => {
65
65
  return new Command('logs')
66
66
  .description(`Tail instance logs`)
67
- .argument('[instance]', 'Instance ID', defaultInstanceId())
67
+ .argument('[instance]', 'Instance ID', savedInstanceId())
68
68
  .action((instance) => {
69
69
  watchInstanceLog(instance, (log) => {
70
70
  const { time, message, stream } = log
package/src/index.ts CHANGED
@@ -1,22 +1,11 @@
1
- #!/usr/bin/env tsx
2
- import { program } from 'commander'
3
- import { version } from '../package.json'
4
- import { DevCommand } from './commands/DevCommand'
5
- import { LinkCommand } from './commands/LinkCommand'
6
- import { ListCommand } from './commands/ListCommand'
7
- import { LoginCommand } from './commands/LoginCommand'
8
- import { LogsCommand } from './commands/LogsCommand'
9
- import { WhoAmICommand } from './commands/WhoAmICommand'
1
+ import { config } from './lib/config'
10
2
 
11
- program
12
- .name(`PocketHost CLI`)
13
- .version(version)
14
- .description(`CLI access to phio`)
15
- .addCommand(LoginCommand())
16
- .addCommand(LogsCommand())
17
- .addCommand(DevCommand())
18
- .addCommand(WhoAmICommand())
19
- .addCommand(ListCommand())
20
- .addCommand(LinkCommand())
3
+ export const isLoggedIn = () => {
4
+ return !!config('auth')
5
+ }
21
6
 
22
- program.parseAsync(process.argv).catch(console.error)
7
+ export * from './commands/DevCommand'
8
+ export * from './commands/LinkCommand'
9
+ export * from './commands/LoginCommand'
10
+ export { config } from './lib/config'
11
+ export { getClient } from './lib/getClient'
package/src/lib/config.ts CHANGED
@@ -3,6 +3,7 @@ import { type AuthModel } from 'pocketbase'
3
3
  import { PHIO_HOME } from './constants'
4
4
 
5
5
  export type Config = {
6
+ instanceId: string
6
7
  email: string
7
8
  auth: { record: AuthModel; token: string }
8
9
  }
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'fs'
2
2
 
3
- export const defaultInstanceId = () => {
3
+ export const savedInstanceId = () => {
4
4
  if (existsSync('package.json')) {
5
5
  const pkg = JSON.parse(readFileSync('package.json').toString())
6
6
  if (pkg.pockethost?.instanceId) {