phio 0.3.3 → 0.3.4

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,7 @@
4
4
 
5
5
  ```bash
6
6
  bunx phio login
7
+ bunx phio logout
7
8
  bunx phio whoami
8
9
  ```
9
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phio",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "A CLI tool to manage your PocketHost instances",
5
5
  "repository": {
6
6
  "type": "git",
@@ -41,7 +41,7 @@
41
41
  "@inquirer/prompts": "^7.3.2",
42
42
  "@s-libs/micro-dash": "^18.0.0",
43
43
  "@samkirkland/ftp-deploy": "github:benallfree/ftp-deploy#132389e",
44
- "@sentool/fetch-event-source": "^0.6.0",
44
+ "@sentool/fetch-event-source": "github:pockethost/sentool-fetch-event-source#c975adc3cdf9c645bf094b0b8a28454699075e22",
45
45
  "bottleneck": "^2.19.5",
46
46
  "chokidar": "^4.0.3",
47
47
  "commander": "^13.1.0",
package/src/cli.ts CHANGED
@@ -7,6 +7,7 @@ import { InfoCommand } from './commands/InfoCommand'
7
7
  import { LinkCommand } from './commands/LinkCommand'
8
8
  import { ListCommand } from './commands/ListCommand'
9
9
  import { LoginCommand } from './commands/LoginCommand'
10
+ import { LogoutCommand } from './commands/LogoutCommand'
10
11
  import { LogsCommand } from './commands/LogsCommand'
11
12
  import { WhoAmICommand } from './commands/WhoAmICommand'
12
13
 
@@ -21,6 +22,7 @@ program
21
22
  .addCommand(ListCommand())
22
23
  .addCommand(LinkCommand())
23
24
  .addCommand(DeployCommand())
25
+ .addCommand(LogoutCommand())
24
26
  .addCommand(InfoCommand())
25
27
 
26
28
  // Add error handling
@@ -21,8 +21,8 @@ export const DeployCommand = () => {
21
21
  },
22
22
  DEFAULT_EXCLUDES
23
23
  )
24
- .action((instanceName, options) => {
24
+ .action(async (instanceName, options) => {
25
25
  const { include, exclude, verbose } = options
26
- deployMyCode(instanceName, include, exclude, verbose)
26
+ await deployMyCode(instanceName, include, exclude, verbose)
27
27
  })
28
28
  }
@@ -40,6 +40,8 @@ export const watchAndDeploy = async (
40
40
  )
41
41
  }
42
42
  console.log(`Dev mode`)
43
+ await ensureLoggedIn()
44
+
43
45
  const { include, exclude, verbose } = options
44
46
  // console.log({ include, exclude })
45
47
 
@@ -81,7 +83,9 @@ export const watchAndDeploy = async (
81
83
  },
82
84
  })
83
85
  console.log(
84
- `Watching for changes in ${include.join(', ')} and excluding ${exclude.join(', ')}`
86
+ `Watching for changes in ${include.join(', ')} and excluding ${exclude.join(
87
+ ', '
88
+ )}`
85
89
  )
86
90
  const handle = (path: string, details: any) => {
87
91
  upload()
@@ -103,7 +107,7 @@ export async function deployMyCode(
103
107
  const args: IFtpDeployArguments = {
104
108
  server: 'ftp.pockethost.io',
105
109
  username: `__auth__`,
106
- password: client.authStore.token,
110
+ password: client.authStore.exportToCookie(),
107
111
  'server-dir': `${instanceName}/`,
108
112
  include,
109
113
  exclude: [...excludeDefaults, ...exclude],
@@ -1,4 +1,5 @@
1
1
  import { Command } from 'commander'
2
+ import { ensureLoggedIn } from '../lib/ensureLoggedIn'
2
3
  import { getClient } from './../lib/getClient'
3
4
  import { InstanceFields } from './../lib/InstanceFields'
4
5
 
@@ -7,6 +8,7 @@ export const ListCommand = () => {
7
8
  .alias(`ls`)
8
9
  .description(`List all the logs`)
9
10
  .action(async () => {
11
+ await ensureLoggedIn()
10
12
  const client = await getClient()
11
13
  const instances = await client
12
14
  .collection(`instances`)
@@ -33,10 +33,7 @@ export const loginWithUserInput = async () => {
33
33
  try {
34
34
  const authStore = await login(email, pw)
35
35
 
36
- config(`auth`, {
37
- token: authStore.exportToCookie(),
38
- record: authStore.model,
39
- })
36
+ config(`pb_auth`, authStore.exportToCookie())
40
37
  } catch (e) {
41
38
  console.error(
42
39
  `There was an error logging in. Please try again or go to https://pockethost.io to reset your password. (${e})`
@@ -0,0 +1,13 @@
1
+ import { Command } from 'commander'
2
+ import { config } from '../lib/config'
3
+
4
+ export const logout = async () => {
5
+ config(`pb_auth`, '')
6
+ console.log(`Logged out!`)
7
+ }
8
+
9
+ export const LogoutCommand = () =>
10
+ new Command('logout')
11
+ .description(`Log out of PocketHost`)
12
+ .helpOption(false)
13
+ .action(logout)
@@ -1,8 +1,8 @@
1
1
  import { fetchEventSource } from '@sentool/fetch-event-source'
2
2
  import { Command } from 'commander'
3
- import { config } from '../lib/config'
4
3
  import { savedInstanceName } from '../lib/defaultInstanceId'
5
4
  import { ensureLoggedIn } from '../lib/ensureLoggedIn'
5
+ import { getClient, getInstanceBySubdomainCnameOrId } from '../lib/getClient'
6
6
 
7
7
  export enum StreamNames {
8
8
  StdOut = 'stdout',
@@ -15,72 +15,192 @@ export type InstanceLogFields = {
15
15
  stream: StreamNames
16
16
  }
17
17
 
18
+ type EventSourceMessage = {
19
+ data: InstanceLogFields | null
20
+ }
21
+
18
22
  type Unsubscribe = () => void
19
23
 
24
+ /**
25
+ * Watches instance logs and streams them to the provided update callback
26
+ *
27
+ * @param instanceName - The instance ID or subdomain to watch logs for
28
+ * @param update - Callback function that will receive log entries
29
+ * @param nInitial - Number of initial log entries to fetch
30
+ * @returns A tuple with [promise that resolves when streaming ends, function to unsubscribe]
31
+ */
20
32
  const watchInstanceLog = async (
21
- instanceId: string,
33
+ instanceName: string,
22
34
  update: (log: InstanceLogFields) => void,
23
35
  nInitial = 100
24
- ): Promise<Unsubscribe> => {
36
+ ): Promise<[Promise<void>, Unsubscribe]> => {
25
37
  const controller = new AbortController()
26
38
  const signal = controller.signal
39
+ let isAborting = false
40
+ let retryTimeout: ReturnType<typeof setTimeout> | null = null
27
41
 
28
42
  await ensureLoggedIn()
43
+ const client = await getClient()
29
44
 
30
- const continuallyFetchFromEventSource = () => {
31
- const url = `https://${instanceId}.pockethost.io/logs`
32
- const body = {
33
- instanceId,
34
- n: nInitial,
35
- auth: config(`auth`)!.token,
36
- }
45
+ try {
46
+ await getInstanceBySubdomainCnameOrId(instanceName)
47
+ } catch (e) {
48
+ throw new Error(`Instance "${instanceName}" not found.`)
49
+ }
37
50
 
38
- fetchEventSource(url, {
39
- method: 'POST',
40
- headers: {
41
- 'Content-Type': 'application/json',
42
- },
43
- openWhenHidden: true,
44
- body: JSON.stringify(body),
45
- onmessage: (event: any) => {
46
- const { data } = event
47
- if (!data) return
48
- update(data)
49
- },
50
- onopen: async (response: Response) => {
51
- // console.log(response)
52
- },
53
- onerror: (e: Error) => {
54
- setTimeout(continuallyFetchFromEventSource, 100)
55
- },
56
- onclose: () => {
57
- console.log(`closed`)
58
- setTimeout(continuallyFetchFromEventSource, 100)
59
- },
60
- signal,
61
- })
51
+ // Function to clear any pending timeouts
52
+ const clearPendingTimeouts = () => {
53
+ if (retryTimeout) {
54
+ clearTimeout(retryTimeout)
55
+ retryTimeout = null
56
+ }
62
57
  }
63
- continuallyFetchFromEventSource()
64
58
 
65
- return () => {
59
+ // Create promise that will resolve when streaming ends
60
+ const streamingPromise = new Promise<void>((resolve, reject) => {
61
+ const continuallyFetchFromEventSource = () => {
62
+ // Don't attempt to reconnect if we're aborting
63
+ if (isAborting) {
64
+ resolve()
65
+ return
66
+ }
67
+
68
+ const url = `https://${instanceName}.pockethost.io/logs`
69
+ const body = {
70
+ instanceId: instanceName,
71
+ n: nInitial,
72
+ auth: client.authStore.exportToCookie(),
73
+ }
74
+
75
+ fetchEventSource(url, {
76
+ method: 'POST',
77
+ headers: {
78
+ 'Content-Type': 'application/json',
79
+ },
80
+ openWhenHidden: true,
81
+ body: JSON.stringify(body),
82
+ onmessage: (event: EventSourceMessage) => {
83
+ const { data } = event
84
+ if (!data || isAborting) return
85
+ update(data)
86
+ },
87
+ onopen: async (response: Response) => {
88
+ if (!response.ok) {
89
+ const error = `Failed to open log stream: ${response.status} ${response.statusText}`
90
+ reject(new Error(error))
91
+ }
92
+ },
93
+ onerror: (e: Error) => {
94
+ if (isAborting) return
95
+ console.error(`Log stream error: ${e}`)
96
+
97
+ // Clear any existing timeout before setting a new one
98
+ clearPendingTimeouts()
99
+ retryTimeout = setTimeout(continuallyFetchFromEventSource, 100)
100
+ },
101
+ onclose: () => {
102
+ if (isAborting) return
103
+ console.log(`Log stream closed. Reconnecting...`)
104
+
105
+ // Clear any existing timeout before setting a new one
106
+ clearPendingTimeouts()
107
+ retryTimeout = setTimeout(continuallyFetchFromEventSource, 100)
108
+ },
109
+ onabort: () => {
110
+ console.log(`Log stream aborted`)
111
+ clearPendingTimeouts()
112
+ resolve()
113
+ },
114
+ signal,
115
+ })
116
+ }
117
+
118
+ // Start the initial connection
119
+ continuallyFetchFromEventSource()
120
+ })
121
+
122
+ // Create clean-up function
123
+ const unsubscribe = () => {
124
+ isAborting = true
125
+ clearPendingTimeouts()
66
126
  controller.abort()
67
127
  }
128
+
129
+ // Add cleanup logic to run when promise completes
130
+ const wrappedPromise = streamingPromise
131
+ .catch((error) => {
132
+ unsubscribe()
133
+ throw error
134
+ })
135
+ .finally(() => {
136
+ clearPendingTimeouts()
137
+ })
138
+
139
+ return [wrappedPromise, unsubscribe]
68
140
  }
69
141
 
142
+ /**
143
+ * Creates the logs command
144
+ */
70
145
  export const LogsCommand = () => {
71
146
  return new Command('logs')
72
- .description(`Tail instance logs`)
147
+ .description('Tail instance logs')
73
148
  .argument('[instance]', 'Instance ID', savedInstanceName())
74
- .action((instance) => {
75
- watchInstanceLog(instance, (log) => {
76
- const { time, message, stream } = log
77
- if (stream === 'stderr') {
78
- console.error(`[${time}] ${message}`)
79
- } else {
80
- console.log(`[${time}] ${message}`)
81
- }
82
- }).catch((e) => {
83
- console.error(`Error fetching logs`, e)
84
- })
149
+ .option(
150
+ '-n, --lines <number>',
151
+ 'Number of initial log lines to show',
152
+ '100'
153
+ )
154
+ .action(async (instance, options) => {
155
+ let running = true
156
+ const nInitial = parseInt(options.lines, 10)
157
+
158
+ // Set up signal handling before starting stream
159
+ const cleanup = () => {
160
+ running = false
161
+ console.log('\nStopping log streaming...')
162
+
163
+ // We'll set this before unsubscribe is defined, but it will be assigned
164
+ // immediately after watchInstanceLog completes
165
+ if (unsubscribe) unsubscribe()
166
+ }
167
+
168
+ // Handle termination signals
169
+ const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGHUP']
170
+ signals.forEach((signal) => process.on(signal, cleanup))
171
+
172
+ // Declare unsubscribe variable that will be defined after watchInstanceLog
173
+ let unsubscribe: Unsubscribe | undefined
174
+
175
+ try {
176
+ // Start watching logs
177
+ const [streamPromise, unsubscribeFn] = await watchInstanceLog(
178
+ instance,
179
+ (log) => {
180
+ // Only process logs if we're still running
181
+ if (!running) return
182
+
183
+ const { time, message, stream } = log
184
+ if (stream === StreamNames.StdErr) {
185
+ console.error(`[${time}] ${message}`)
186
+ } else {
187
+ console.log(`[${time}] ${message}`)
188
+ }
189
+ },
190
+ nInitial
191
+ )
192
+
193
+ // Store unsubscribe function for use in cleanup
194
+ unsubscribe = unsubscribeFn
195
+
196
+ // Wait for the streaming to end naturally (should only happen on abort)
197
+ await streamPromise
198
+ } catch (err) {
199
+ cleanup()
200
+ throw err
201
+ } finally {
202
+ // Remove signal handlers to prevent memory leaks
203
+ signals.forEach((signal) => process.removeListener(signal, cleanup))
204
+ }
85
205
  })
86
206
  }
package/src/lib/config.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  import fse from 'fs-extra'
2
- import { type AuthModel } from 'pocketbase'
3
2
  import { PHIO_HOME } from './constants'
4
3
 
5
4
  const { readJSONSync, writeJSONSync } = fse
6
5
  export type Config = {
7
6
  email: string
8
- auth: { record: AuthModel; token: string }
7
+ pb_auth: string
9
8
  }
10
9
  export function config<T extends keyof Config>(
11
10
  k: T,
@@ -2,6 +2,7 @@ import PocketBase from 'pocketbase'
2
2
  import { runTasks } from './../lib/Task'
3
3
  import { config } from './config'
4
4
  import { PHIO_MOTHERSHIP_URL, PHIO_PASSWORD, PHIO_USERNAME } from './constants'
5
+ import { ensureLoggedIn } from './ensureLoggedIn'
5
6
 
6
7
  let client: PocketBase | undefined
7
8
  export const getClient = async () => {
@@ -21,31 +22,25 @@ export const getClient = async () => {
21
22
  }
22
23
  }
23
24
 
24
- const authStore = config('auth')
25
- if (authStore) {
26
- const { record, token } = authStore
27
- client.authStore.loadFromCookie(token)
25
+ const cookie = config('pb_auth')
26
+ if (cookie) {
27
+ client.authStore.loadFromCookie(cookie)
28
28
  // console.log({ valid: client.authStore.isValid })
29
29
  client.authStore.onChange((token, record) => {
30
30
  if (!client) {
31
31
  console.warn('No client found - please report this bug')
32
32
  return
33
33
  }
34
- config('auth', {
35
- token: client.authStore.exportToCookie(),
36
- record: client.authStore.model,
37
- })
34
+ config('pb_auth', client.authStore.exportToCookie())
38
35
  })
39
36
  await client.collection(`users`).authRefresh()
40
- config(`auth`, {
41
- token: client.authStore.exportToCookie(),
42
- record: client.authStore.model,
43
- })
37
+ config(`pb_auth`, client.authStore.exportToCookie())
44
38
  }
45
39
  return client
46
40
  }
47
41
 
48
42
  export const getInstanceBySubdomainCnameOrId = async (search: string) => {
43
+ await ensureLoggedIn()
49
44
  const client = await getClient()
50
45
  return await client
51
46
  .collection(`instances`)