phio 0.0.2 → 0.1.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 CHANGED
@@ -4,6 +4,13 @@
4
4
 
5
5
  ```bash
6
6
  bunx phio login
7
+ bunx phio whoami
8
+ ```
9
+
10
+ **List instances**
11
+
12
+ ```bash
13
+ bunx phio list
7
14
  ```
8
15
 
9
16
  **Watch and push local changes instantly**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phio",
3
- "version": "0.0.2",
3
+ "version": "0.1.1",
4
4
  "description": "A CLI tool to manage your PocketHost instances",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,7 +22,8 @@
22
22
  "devDependencies": {
23
23
  "@types/bun": "latest",
24
24
  "@types/fs-extra": "^11.0.4",
25
- "@types/node": "^22.5.5"
25
+ "@types/node": "^22.5.5",
26
+ "prettier-plugin-organize-imports": "^4.0.0"
26
27
  },
27
28
  "peerDependencies": {
28
29
  "typescript": "^5.6.2"
@@ -39,8 +40,9 @@
39
40
  "dependencies": {
40
41
  "@inquirer/prompts": "^5.5.0",
41
42
  "@s-libs/micro-dash": "^18.0.0",
42
- "@samkirkland/ftp-deploy": "^1.2.4",
43
+ "@samkirkland/ftp-deploy": "github:benallfree/ftp-deploy#glob",
43
44
  "@sentool/fetch-event-source": "^0.5.0",
45
+ "bottleneck": "^2.19.5",
44
46
  "chokidar": "^4.0.0",
45
47
  "commander": "^12.1.0",
46
48
  "email-validator": "^2.0.4",
@@ -48,8 +50,17 @@
48
50
  "env-var": "^7.5.0",
49
51
  "event-stream": "^4.0.1",
50
52
  "fs-extra": "^11.2.0",
53
+ "multimatch": "^7.0.0",
51
54
  "ora": "^8.1.0",
52
55
  "pocketbase": "^0.21.5",
53
56
  "tsx": "^4.19.1"
57
+ },
58
+ "prettier": {
59
+ "semi": false,
60
+ "singleQuote": true,
61
+ "trailingComma": "es5",
62
+ "plugins": [
63
+ "prettier-plugin-organize-imports"
64
+ ]
54
65
  }
55
66
  }
@@ -0,0 +1,108 @@
1
+ import { debounce } from '@s-libs/micro-dash'
2
+ import { deploy, excludeDefaults } from '@samkirkland/ftp-deploy'
3
+ import Bottleneck from 'bottleneck'
4
+ import { watch } from 'chokidar'
5
+ import { Command } from 'commander'
6
+ import { ensureDirSync } from 'fs-extra'
7
+ import multimatch from 'multimatch'
8
+ import { config } from '../lib/config'
9
+ import { getInstanceBySubdomainCnameOrId } from '../lib/getClient'
10
+ import { defaultInstanceId } from './../lib/defaultInstanceId'
11
+
12
+ async function deployMyCode(
13
+ instanceName: string,
14
+ include: string[],
15
+ exclude: string[]
16
+ ) {
17
+ const cachePath = '.cache'
18
+ ensureDirSync(cachePath)
19
+
20
+ console.log('🚚 Deploy started')
21
+ await deploy({
22
+ server: 'ftp.pockethost.io',
23
+ username: `__auth__`,
24
+ password: config(`auth`)!.token,
25
+ 'server-dir': `${instanceName}/`,
26
+ include,
27
+ exclude: [...excludeDefaults, ...exclude],
28
+ 'state-name': '.cache/.ftp-deploy-sync-state.json',
29
+ 'log-level': 'verbose',
30
+ })
31
+ console.log('🚀 Deploy done!')
32
+ }
33
+
34
+ export const DevCommand = () => {
35
+ return new Command('dev')
36
+ .argument(`[instanceId]`, `Instance name`, defaultInstanceId())
37
+ .description(`Watch for local modifications and sync to remote`)
38
+ .option(
39
+ '-i, --include <include...>',
40
+ 'Files to include in the sync',
41
+ (val, prev) => [...prev, val],
42
+ [
43
+ `pb_*`,
44
+ 'pb_*/**/*',
45
+ `package.json`,
46
+ `bun.lockb`,
47
+ `patches`,
48
+ `patches/**/*`,
49
+ ]
50
+ )
51
+ .option(
52
+ '-e, --exclude <exclude...>',
53
+ 'Files to exclude from the sync',
54
+ (val, prev) => [...prev, val],
55
+ [`pb_data`, `pb_data/**/*`]
56
+ )
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
+ })
108
+ }
@@ -0,0 +1,21 @@
1
+ import { Command } from 'commander'
2
+ import { readJSONSync, writeJSONSync } from 'fs-extra/esm'
3
+ import { getInstanceBySubdomainCnameOrId } from './../lib/getClient'
4
+
5
+ export const LinkCommand = () => {
6
+ return new Command(`link`)
7
+ .argument(`<instance>`, `Instance name or ID`)
8
+ .description(`Link a local directory to a remote instance`)
9
+ .action(async (_anyName) => {
10
+ const instance = await getInstanceBySubdomainCnameOrId(_anyName)
11
+ if (!instance) {
12
+ console.error(`Instance not found`)
13
+ process.exit(1)
14
+ }
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 })
20
+ })
21
+ }
@@ -0,0 +1,21 @@
1
+ import { Command } from 'commander'
2
+ import { getClient } from './../lib/getClient'
3
+ import { InstanceFields } from './../lib/InstanceFields'
4
+
5
+ export const ListCommand = () => {
6
+ return new Command(`list`)
7
+ .description(`List all the logs`)
8
+ .action(async () => {
9
+ const client = getClient()
10
+ const instances = await client
11
+ .collection(`instances`)
12
+ .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
+ })
20
+ })
21
+ }
@@ -1,10 +1,9 @@
1
1
  import { input, password } from '@inquirer/prompts'
2
2
  import { Command } from 'commander'
3
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'
4
+ import { config } from '../lib/config'
5
+ import { getClient } from './../lib/getClient'
6
+ import { runTasks } from './../lib/Task'
8
7
 
9
8
  export const LoginCommand = () =>
10
9
  new Command('login')
@@ -38,7 +37,6 @@ export const LoginCommand = () =>
38
37
  const res = await client
39
38
  .collection('users')
40
39
  .authWithPassword(email, pw)
41
- console.log({ res })
42
40
  },
43
41
  },
44
42
  ])
@@ -1,7 +1,7 @@
1
- import { Command } from 'commander'
2
- import { config } from './config'
3
- //@ts-ignore
4
1
  import { fetchEventSource } from '@sentool/fetch-event-source'
2
+ import { Command } from 'commander'
3
+ import { config } from '../lib/config'
4
+ import { defaultInstanceId } from '../lib/defaultInstanceId'
5
5
 
6
6
  export enum StreamNames {
7
7
  StdOut = 'stdout',
@@ -45,6 +45,7 @@ const watchInstanceLog = (
45
45
  },
46
46
  onerror: (e: Error) => {
47
47
  console.error(`got an error`, e)
48
+ setTimeout(continuallyFetchFromEventSource, 100)
48
49
  },
49
50
  onclose: () => {
50
51
  console.log(`closed`)
@@ -63,7 +64,7 @@ const watchInstanceLog = (
63
64
  export const LogsCommand = () => {
64
65
  return new Command('logs')
65
66
  .description(`Tail instance logs`)
66
- .argument('<instance>', 'Instance ID')
67
+ .argument('[instance]', 'Instance ID', defaultInstanceId())
67
68
  .action((instance) => {
68
69
  watchInstanceLog(instance, (log) => {
69
70
  const { time, message, stream } = log
@@ -0,0 +1,15 @@
1
+ import { Command } from 'commander'
2
+ import { config } from '../lib/config'
3
+
4
+ export const WhoAmICommand = () => {
5
+ return new Command(`whoami`)
6
+ .description(`Show the current user`)
7
+ .action(() => {
8
+ const email = config(`email`)
9
+ if (email) {
10
+ console.log(`Current user is: ${email}`)
11
+ return
12
+ }
13
+ console.log(`No user is currently logged in`)
14
+ })
15
+ }
package/src/global.d.ts CHANGED
@@ -1,6 +1,3 @@
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 }
1
+ declare module '@sentool/fetch-event-source' {
2
+ export function fetchEventSource(url: string, options: any): void
6
3
  }
package/src/index.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { program } from 'commander'
3
- import { LoginCommand } from './LoginCommand'
4
3
  import { version } from '../package.json'
5
- import { LogsCommand } from './LogsCommand'
6
- import { DevCommand } from './DevCommand'
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'
7
10
 
8
11
  program
9
12
  .name(`PocketHost CLI`)
@@ -12,5 +15,8 @@ program
12
15
  .addCommand(LoginCommand())
13
16
  .addCommand(LogsCommand())
14
17
  .addCommand(DevCommand())
18
+ .addCommand(WhoAmICommand())
19
+ .addCommand(ListCommand())
20
+ .addCommand(LinkCommand())
15
21
 
16
22
  program.parseAsync(process.argv).catch(console.error)
@@ -0,0 +1,6 @@
1
+ export type InstanceFields = {
2
+ id: string
3
+ subdomain: string
4
+ cname: string
5
+ status: string
6
+ }
@@ -15,3 +15,12 @@ export const getClient = () => {
15
15
  }
16
16
  return client
17
17
  }
18
+
19
+ export const getInstanceBySubdomainCnameOrId = async (search: string) => {
20
+ const client = getClient()
21
+ return await client
22
+ .collection(`instances`)
23
+ .getFirstListItem(
24
+ `id='${search}' || subdomain='${search}' || cname='${search}'`
25
+ )
26
+ }
package/src/DevCommand.ts DELETED
@@ -1,67 +0,0 @@
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
- }
File without changes
File without changes
File without changes