phio 0.3.3 → 0.3.5
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 +1 -0
- package/package.json +2 -2
- package/src/cli.ts +2 -0
- package/src/commands/DeployCommand.ts +2 -2
- package/src/commands/DevCommand.ts +7 -2
- package/src/commands/ListCommand.ts +2 -0
- package/src/commands/LoginCommand.ts +1 -4
- package/src/commands/LogoutCommand.ts +13 -0
- package/src/commands/LogsCommand.ts +168 -48
- package/src/lib/config.ts +1 -2
- package/src/lib/getClient.ts +7 -12
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phio",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
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": "
|
|
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
|
}
|
|
@@ -14,6 +14,7 @@ export const DEFAULT_INCLUDES = [
|
|
|
14
14
|
'pb_*/**/*',
|
|
15
15
|
`package.json`,
|
|
16
16
|
`bun.lockb`,
|
|
17
|
+
`bun.lock`,
|
|
17
18
|
`patches`,
|
|
18
19
|
`patches/**/*`,
|
|
19
20
|
]
|
|
@@ -40,6 +41,8 @@ export const watchAndDeploy = async (
|
|
|
40
41
|
)
|
|
41
42
|
}
|
|
42
43
|
console.log(`Dev mode`)
|
|
44
|
+
await ensureLoggedIn()
|
|
45
|
+
|
|
43
46
|
const { include, exclude, verbose } = options
|
|
44
47
|
// console.log({ include, exclude })
|
|
45
48
|
|
|
@@ -81,7 +84,9 @@ export const watchAndDeploy = async (
|
|
|
81
84
|
},
|
|
82
85
|
})
|
|
83
86
|
console.log(
|
|
84
|
-
`Watching for changes in ${include.join(', ')} and excluding ${exclude.join(
|
|
87
|
+
`Watching for changes in ${include.join(', ')} and excluding ${exclude.join(
|
|
88
|
+
', '
|
|
89
|
+
)}`
|
|
85
90
|
)
|
|
86
91
|
const handle = (path: string, details: any) => {
|
|
87
92
|
upload()
|
|
@@ -103,7 +108,7 @@ export async function deployMyCode(
|
|
|
103
108
|
const args: IFtpDeployArguments = {
|
|
104
109
|
server: 'ftp.pockethost.io',
|
|
105
110
|
username: `__auth__`,
|
|
106
|
-
password: client.authStore.
|
|
111
|
+
password: client.authStore.exportToCookie(),
|
|
107
112
|
'server-dir': `${instanceName}/`,
|
|
108
113
|
include,
|
|
109
114
|
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(`
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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(
|
|
147
|
+
.description('Tail instance logs')
|
|
73
148
|
.argument('[instance]', 'Instance ID', savedInstanceName())
|
|
74
|
-
.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
7
|
+
pb_auth: string
|
|
9
8
|
}
|
|
10
9
|
export function config<T extends keyof Config>(
|
|
11
10
|
k: T,
|
package/src/lib/getClient.ts
CHANGED
|
@@ -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
|
|
25
|
-
if (
|
|
26
|
-
|
|
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('
|
|
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(`
|
|
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`)
|