phio 0.3.5 → 0.4.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/CHANGELOG.md ADDED
@@ -0,0 +1,141 @@
1
+ # phio
2
+
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - SFTP deploy and dev sync (`ftp.pockethost.io:2222`, Ed25519 key auth)
8
+ - `phio sftp` — interactive SFTP via system client (`--print` for the command line)
9
+ - Auto-provision deploy key labeled `Phio` under Account → Keys
10
+ - Project instance linking via `.phioconfig` (migrates legacy `package.json` / `pockethost.json`)
11
+ - Walk up to project root from subdirectories before sync
12
+
13
+ ## 0.3.5
14
+
15
+ ### Patch Changes
16
+
17
+ - Fix: add support for bun.lock
18
+
19
+ ## 0.3.4
20
+
21
+ ### Patch Changes
22
+
23
+ - 24988ee: Feat: logout command
24
+ - de7168b: Fix: various auth fixes
25
+ - 6262058: Fix: use correct token for log watching
26
+ - 5d28256: Enhance logs command with improved streaming and error handling
27
+
28
+ ## 0.3.3
29
+
30
+ ### Patch Changes
31
+
32
+ - 4bf6d1e: Fix: use client auth token instead of stored token
33
+
34
+ ## 0.3.2
35
+
36
+ ### Patch Changes
37
+
38
+ - b85e156: Fix: nodev22 compat
39
+
40
+ ## 0.3.1
41
+
42
+ ### Patch Changes
43
+
44
+ - fd128bc: Node v22 compat
45
+
46
+ ## 0.3.0
47
+
48
+ ### Minor Changes
49
+
50
+ - 82b4c6b: Add support for PHIO_USERNAME, PHIO_PASSWORD, and PHIO_INSTANCE_NAME env vars
51
+ - 82b4c6b: Migrate to instance names only (no IDs)
52
+ - 82b4c6b: Remove support for global instance name - local package.json or pockethost.json required now
53
+
54
+ ### Patch Changes
55
+
56
+ - 1b46e08: Alphabetize instance lists
57
+ - 82b4c6b: Improved error message reporting
58
+
59
+ ## 0.2.5
60
+
61
+ ### Patch Changes
62
+
63
+ - 283da4e: Add 'info' command
64
+ - b17fca2: Add error handling when 'link' returns no instances
65
+ - 8aaa611: Add 'ls' alias
66
+
67
+ ## 0.2.4
68
+
69
+ ### Patch Changes
70
+
71
+ - Fix: --include and --exclude now parse comma separated values correctly
72
+
73
+ ## 0.2.3
74
+
75
+ ### Patch Changes
76
+
77
+ - Fix: bad include path
78
+
79
+ ## 0.2.2
80
+
81
+ ### Patch Changes
82
+
83
+ - 467c4ba: Enh: Validate auth before tailing instance logs
84
+ - 4e8ddb5: Fix: auth token now stored correctly after refresh
85
+
86
+ ## 0.2.1
87
+
88
+ ### Patch Changes
89
+
90
+ - Fix: Link now uses package.json
91
+
92
+ ## 0.2.0
93
+
94
+ ### Minor Changes
95
+
96
+ - Introduced deploy command
97
+
98
+ ### Patch Changes
99
+
100
+ - Command refactoring
101
+
102
+ ## 0.1.2
103
+
104
+ ### Patch Changes
105
+
106
+ - Enh: add --verbose flag to dev mode
107
+
108
+ ## 0.1.1
109
+
110
+ ### Patch Changes
111
+
112
+ - Fix: include and exclude defaults
113
+
114
+ ## 0.1.0
115
+
116
+ ### Minor Changes
117
+
118
+ - afc6e91: Added `link` command to link to a specific instance
119
+ - fff79df: pockethost.io now runs `bun install` when uploading a `bun.lockb`
120
+ - 9fc57d6: Added whoami command
121
+ - 9fc57d6: Added list command
122
+ - afc6e91: Added --include and --exclude options to dev watch mode
123
+
124
+ ### Patch Changes
125
+
126
+ - afc6e91: Fix: log tailer will now restart on disconnect
127
+ - fff79df: Enh: watcher now looks for package.json and bun.lockb
128
+ - fff79df: Fix: watcher was incorrectly applying --include and --exclude
129
+ - fff79df: Enh: watcher now queues successive deployments that happen in rapid succession
130
+
131
+ ## 0.0.2
132
+
133
+ ### Patch Changes
134
+
135
+ - Forgot tsx dep
136
+
137
+ ## 0.0.1
138
+
139
+ ### Patch Changes
140
+
141
+ - a5bf2f3: Initial version
package/README.md CHANGED
@@ -1,66 +1,75 @@
1
- # phio: the pockethost.io CLI
1
+ # phio: the PocketHost CLI
2
2
 
3
- **Auth**
3
+ Install globally (Node.js 24+):
4
4
 
5
5
  ```bash
6
- bunx phio login
7
- bunx phio logout
8
- bunx phio whoami
6
+ npm install -g phio
9
7
  ```
10
8
 
11
- **List instances**
9
+ From the monorepo:
12
10
 
13
11
  ```bash
14
- bunx phio list
12
+ pnpm dev:phio -- --help
13
+ pnpm --filter phio dev -- login
15
14
  ```
16
15
 
17
- **Watch and push local changes instantly**
16
+ ## Commands
18
17
 
19
18
  ```bash
20
- bunx phio dev [instance]
19
+ phio login
20
+ phio logout
21
+ phio info # alias: phio whoami
22
+ phio list
23
+ phio link [instance]
24
+ phio dev [instance]
25
+ phio deploy [instance]
26
+ phio sftp [instance]
27
+ phio logs [instance]
21
28
  ```
22
29
 
23
- **Deploy to remote**
30
+ - **`phio dev`** — watch local changes and sync over SFTP
31
+ - **`phio deploy`** — one-shot SFTP sync
32
+ - **`phio sftp`** — interactive SFTP session (uses system `sftp` when available)
33
+ - **`phio logs`** — tail instance logs
24
34
 
25
- ```bash
26
- bunx phio deploy [instance]
27
- ```
35
+ `dev` and `deploy` accept `-v/--verbose`, `-i/--include`, and `-e/--exclude`.
28
36
 
29
- **Tail logs**
37
+ Default includes: `pb_*`, `package.json`, `bun.lock`, `bun.lockb`, `patches/**`.
30
38
 
31
- ```bash
32
- bunx phio logs [instance]
33
- ```
39
+ Default excludes: `pb_data/**`.
34
40
 
35
41
  ## Configuration
36
42
 
37
- Use `pockethost` in your `package.json` to save your instance name so you don't need to keep typing it:
43
+ Link a project directory to an instance:
38
44
 
39
- ```json
40
- // package.json
41
- {
42
- "pockethost": {
43
- "instanceName": "all-your-base"
44
- }
45
- }
45
+ ```bash
46
+ phio link my-instance
46
47
  ```
47
48
 
48
- -or-
49
-
50
- Use `pockethost.json` to save your instance name so you don't need to keep typing it.
49
+ This writes `.phioconfig`:
51
50
 
52
51
  ```json
53
52
  {
54
- "instanceName": "all-your-base"
53
+ "instanceName": "my-instance"
55
54
  }
56
55
  ```
57
56
 
58
- ## Environment Variables
57
+ Legacy `package.json` (`pockethost.instanceName`) and `pockethost.json` are migrated to `.phioconfig` automatically on first use.
58
+
59
+ ## Deploy key
60
+
61
+ `phio dev` and `phio deploy` sync over **SFTP** (`ftp.pockethost.io:2222`) using an Ed25519 deploy key stored under `PHIO_HOME` (default `~/.config/phio/`). phio auto-registers a **`Phio`** key under Account → Keys on first use. Run `phio info` to inspect key status.
62
+
63
+ Customer docs: https://pockethost.io/docs/phio
59
64
 
60
- The following environment variables can be used to override any saved configuration:
65
+ ## Environment variables
61
66
 
62
- - `PHIO_USERNAME` - Override saved username
63
- - `PHIO_PASSWORD` - Override saved password
64
- - `PHIO_INSTANCE_NAME` - Override saved instance name
67
+ | Variable | Purpose |
68
+ | -------- | ------- |
69
+ | `PHIO_USERNAME` | Override saved email (non-interactive login) |
70
+ | `PHIO_PASSWORD` | Override saved password |
71
+ | `PHIO_INSTANCE_NAME` | Override linked instance name |
72
+ | `PHIO_MOTHERSHIP_URL` | Override mothership API URL |
73
+ | `PHIO_HOME` | Override phio config directory |
65
74
 
66
- Environment variables take precedence over configuration in package.json or pockethost.json.
75
+ Environment variables take precedence over `.phioconfig`.
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "phio",
3
- "version": "0.3.5",
3
+ "version": "0.4.0",
4
4
  "description": "A CLI tool to manage your PocketHost instances",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "https://github.com/pockethost/phio.git"
7
+ "url": "https://github.com/pockethost/pockethost.git",
8
+ "directory": "packages/phio"
8
9
  },
9
- "homepage": "https://github.com/pockethost/phio",
10
+ "homepage": "https://pockethost.io",
10
11
  "keywords": [
11
12
  "cli",
12
13
  "pocketbase",
@@ -15,33 +16,34 @@
15
16
  "author": "Ben Allfree",
16
17
  "license": "MIT",
17
18
  "bugs": {
18
- "url": "https://github.com/pockethost/phio/issues"
19
+ "url": "https://github.com/pockethost/pockethost/issues"
19
20
  },
21
+ "type": "module",
20
22
  "main": "src/index.ts",
21
23
  "module": "src/index.ts",
22
- "type": "module",
23
24
  "types": "src/index.ts",
24
- "devDependencies": {
25
- "@changesets/cli": "^2.28.1",
26
- "@types/bun": "^1.2.3",
27
- "@types/fs-extra": "^11.0.4",
28
- "@types/node": "^22.13.5",
29
- "prettier-plugin-organize-imports": "^4.1.0"
30
- },
31
- "scripts": {
32
- "dev": "tsx ./src/cli.ts"
33
- },
34
25
  "bin": {
35
26
  "phio": "src/cli.ts"
36
27
  },
28
+ "engines": {
29
+ "node": ">=24"
30
+ },
31
+ "scripts": {
32
+ "check:types": "tsc --noEmit",
33
+ "dev": "node --import tsx ./src/cli.ts",
34
+ "start": "node --import tsx ./src/cli.ts"
35
+ },
37
36
  "files": [
38
- "src"
37
+ "src",
38
+ "vendor",
39
+ "CHANGELOG.md",
40
+ "README.md"
39
41
  ],
40
42
  "dependencies": {
41
43
  "@inquirer/prompts": "^7.3.2",
44
+ "@microsoft/fetch-event-source": "github:pockethost/fetch-event-source#ebe3b7122647b48b93fd11effbbfb915d98956b0",
42
45
  "@s-libs/micro-dash": "^18.0.0",
43
- "@samkirkland/ftp-deploy": "github:benallfree/ftp-deploy#132389e",
44
- "@sentool/fetch-event-source": "github:pockethost/sentool-fetch-event-source#c975adc3cdf9c645bf094b0b8a28454699075e22",
46
+ "basic-ftp": "^5.0.5",
45
47
  "bottleneck": "^2.19.5",
46
48
  "chokidar": "^4.0.3",
47
49
  "commander": "^13.1.0",
@@ -50,18 +52,26 @@
50
52
  "env-var": "^7.5.0",
51
53
  "event-stream": "^4.0.1",
52
54
  "fs-extra": "^11.3.0",
55
+ "glob": "^11.0.0",
56
+ "lodash": "^4.17.21",
53
57
  "multimatch": "^7.0.0",
54
58
  "ora": "^8.2.0",
55
59
  "pocketbase": "^0.25.1",
60
+ "pretty-bytes": "^5.6.0",
61
+ "pretty-ms": "^7.0.1",
62
+ "ssh2-sftp-client": "^12.1.1",
56
63
  "tsx": "^4.19.3",
64
+ "yargs": "^17.7.1"
65
+ },
66
+ "devDependencies": {
67
+ "@types/fs-extra": "^11.0.4",
68
+ "@types/node": "^24.0.0",
69
+ "@types/ssh2-sftp-client": "^9.0.6",
57
70
  "typescript": "^5.7.3"
58
71
  },
59
72
  "prettier": {
60
73
  "semi": false,
61
74
  "singleQuote": true,
62
- "trailingComma": "es5",
63
- "plugins": [
64
- "prettier-plugin-organize-imports"
65
- ]
75
+ "trailingComma": "es5"
66
76
  }
67
77
  }
package/src/cli.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { program } from 'commander'
3
3
  import { version } from '../package.json'
4
+ import { ensurePhioRoot } from './lib/phioRoot'
4
5
  import { DeployCommand } from './commands/DeployCommand'
5
6
  import { DevCommand } from './commands/DevCommand'
6
7
  import { InfoCommand } from './commands/InfoCommand'
@@ -9,7 +10,9 @@ import { ListCommand } from './commands/ListCommand'
9
10
  import { LoginCommand } from './commands/LoginCommand'
10
11
  import { LogoutCommand } from './commands/LogoutCommand'
11
12
  import { LogsCommand } from './commands/LogsCommand'
12
- import { WhoAmICommand } from './commands/WhoAmICommand'
13
+ import { SftpCommand } from './commands/SftpCommand'
14
+
15
+ ensurePhioRoot()
13
16
 
14
17
  program
15
18
  .name(`PocketHost CLI`)
@@ -18,18 +21,20 @@ program
18
21
  .addCommand(LoginCommand())
19
22
  .addCommand(LogsCommand())
20
23
  .addCommand(DevCommand())
21
- .addCommand(WhoAmICommand())
22
24
  .addCommand(ListCommand())
23
25
  .addCommand(LinkCommand())
24
26
  .addCommand(DeployCommand())
27
+ .addCommand(SftpCommand())
25
28
  .addCommand(LogoutCommand())
26
29
  .addCommand(InfoCommand())
27
30
 
28
31
  // Add error handling
29
32
  program.exitOverride()
30
33
 
31
- program.parseAsync(process.argv).catch((err) => {
32
- // Handle specific commander error types
34
+ program.parseAsync(process.argv).catch((err: NodeJS.ErrnoException & { code?: string }) => {
35
+ if (err.code === 'commander.helpDisplayed' || err.code === 'commander.help' || err.code === 'commander.version') {
36
+ process.exit(0)
37
+ }
33
38
  if (err.code === 'commander.unknownCommand') {
34
39
  console.error('Error: Unknown command')
35
40
  } else if (err.code === 'commander.missingArgument') {
@@ -1,10 +1,13 @@
1
1
  import { debounce } from '@s-libs/micro-dash'
2
- import { deploy, excludeDefaults } from '@samkirkland/ftp-deploy'
3
- import { IFtpDeployArguments } from '@samkirkland/ftp-deploy/src/types'
2
+ import { deploy, excludeDefaults } from '../../vendor/ftp-deploy/module.js'
3
+ import type { IFtpDeployArguments } from '../../vendor/ftp-deploy/types.js'
4
4
  import Bottleneck from 'bottleneck'
5
5
  import { watch } from 'chokidar'
6
6
  import { Command } from 'commander'
7
7
  import multimatch from 'multimatch'
8
+ import { ensureDeployKey } from '../lib/deployKey'
9
+ import { PHIO_CONFIG_FILE } from '../lib/constants'
10
+ import { PHIO_SFTP_HOST, PHIO_SFTP_PORT } from '../lib/sftpConnection'
8
11
  import { ensureLoggedIn } from '../lib/ensureLoggedIn'
9
12
  import { getClient, getInstanceBySubdomainCnameOrId } from '../lib/getClient'
10
13
  import { savedInstanceName } from './../lib/defaultInstanceId'
@@ -37,7 +40,7 @@ export const watchAndDeploy = async (
37
40
  ) => {
38
41
  if (!instanceName) {
39
42
  throw new Error(
40
- `No instance name provided and none was found in package.json or pockethost.json. Use 'phio link <instance>'`
43
+ `No instance name provided and none was found in ${PHIO_CONFIG_FILE}. Use 'phio link <instance>'`
41
44
  )
42
45
  }
43
46
  console.log(`Dev mode`)
@@ -104,11 +107,19 @@ export async function deployMyCode(
104
107
  ) {
105
108
  await ensureLoggedIn()
106
109
  const client = await getClient()
110
+ const { privateKeyPath } = await ensureDeployKey(client)
111
+ const email = client.authStore.record?.email
112
+ if (!email) {
113
+ throw new Error(`You must be logged in first. Use 'phio login'`)
114
+ }
115
+
107
116
  console.log(`🚚 Deploy started for ${instanceName}`)
108
117
  const args: IFtpDeployArguments = {
109
- server: 'ftp.pockethost.io',
110
- username: `__auth__`,
111
- password: client.authStore.exportToCookie(),
118
+ server: PHIO_SFTP_HOST,
119
+ protocol: 'sftp',
120
+ port: PHIO_SFTP_PORT,
121
+ username: email,
122
+ 'private-key-path': privateKeyPath,
112
123
  'server-dir': `${instanceName}/`,
113
124
  include,
114
125
  exclude: [...excludeDefaults, ...exclude],
@@ -1,10 +1,33 @@
1
1
  import { Command } from 'commander'
2
+ import { DEPLOY_KEY_LABEL, ensureDeployKey, formatDeployKeyRemoteStatus, getDeployKeyStatus } from '../lib/deployKey'
2
3
  import { PHIO_HOME } from '../lib/constants'
3
4
  import { savedInstanceName } from '../lib/defaultInstanceId'
5
+ import { resolveAuthStatus } from '../lib/getClient'
6
+
7
+ export const showInfo = async () => {
8
+ const auth = await resolveAuthStatus()
9
+
10
+ if (auth.state !== 'authenticated') {
11
+ console.log(`Not logged in. Run 'phio login'.`)
12
+ return
13
+ }
14
+
15
+ await ensureDeployKey(auth.client)
16
+ const status = await getDeployKeyStatus(auth.client)
17
+
18
+ console.log(`Config root: ${PHIO_HOME()}`)
19
+ console.log(`Logged in as: ${auth.email}`)
20
+ console.log(`Instance: ${savedInstanceName() || '(not linked)'}`)
21
+ console.log(`Deploy key label: ${DEPLOY_KEY_LABEL}`)
22
+ console.log(`Deploy key private: ${status.privateKeyPath}`)
23
+ console.log(`Deploy key public: ${status.publicKeyPath}`)
24
+ console.log(`Deploy key fingerprint: ${status.fingerprint}`)
25
+ console.log(`Deploy key remote: ${formatDeployKeyRemoteStatus(status.remote)}`)
26
+ }
4
27
 
5
28
  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
- })
29
+ return new Command(`info`)
30
+ .alias(`whoami`)
31
+ .description(`Show login and config info`)
32
+ .action(showInfo)
10
33
  }
@@ -5,7 +5,7 @@ import { InstanceFields } from '../lib/InstanceFields'
5
5
  import { getClient, getInstanceBySubdomainCnameOrId } from './../lib/getClient'
6
6
 
7
7
  export const link = async (instanceName: string) => {
8
- saveInstanceName(instanceName, 'package.json')
8
+ saveInstanceName(instanceName)
9
9
  const instance = await getInstanceBySubdomainCnameOrId(instanceName)
10
10
  if (!instance) {
11
11
  return
@@ -1,4 +1,4 @@
1
- import { fetchEventSource } from '@sentool/fetch-event-source'
1
+ import { fetchEventSource } from '../lib/fetchEventSource.js'
2
2
  import { Command } from 'commander'
3
3
  import { savedInstanceName } from '../lib/defaultInstanceId'
4
4
  import { ensureLoggedIn } from '../lib/ensureLoggedIn'
@@ -16,7 +16,7 @@ export type InstanceLogFields = {
16
16
  }
17
17
 
18
18
  type EventSourceMessage = {
19
- data: InstanceLogFields | null
19
+ data: string
20
20
  }
21
21
 
22
22
  type Unsubscribe = () => void
@@ -58,6 +58,11 @@ const watchInstanceLog = async (
58
58
 
59
59
  // Create promise that will resolve when streaming ends
60
60
  const streamingPromise = new Promise<void>((resolve, reject) => {
61
+ signal.addEventListener('abort', () => {
62
+ clearPendingTimeouts()
63
+ resolve()
64
+ })
65
+
61
66
  const continuallyFetchFromEventSource = () => {
62
67
  // Don't attempt to reconnect if we're aborting
63
68
  if (isAborting) {
@@ -80,9 +85,13 @@ const watchInstanceLog = async (
80
85
  openWhenHidden: true,
81
86
  body: JSON.stringify(body),
82
87
  onmessage: (event: EventSourceMessage) => {
83
- const { data } = event
84
- if (!data || isAborting) return
85
- update(data)
88
+ if (isAborting) return
89
+ try {
90
+ const data = JSON.parse(event.data) as InstanceLogFields
91
+ update(data)
92
+ } catch {
93
+ // ignore malformed lines
94
+ }
86
95
  },
87
96
  onopen: async (response: Response) => {
88
97
  if (!response.ok) {
@@ -102,15 +111,9 @@ const watchInstanceLog = async (
102
111
  if (isAborting) return
103
112
  console.log(`Log stream closed. Reconnecting...`)
104
113
 
105
- // Clear any existing timeout before setting a new one
106
114
  clearPendingTimeouts()
107
115
  retryTimeout = setTimeout(continuallyFetchFromEventSource, 100)
108
116
  },
109
- onabort: () => {
110
- console.log(`Log stream aborted`)
111
- clearPendingTimeouts()
112
- resolve()
113
- },
114
117
  signal,
115
118
  })
116
119
  }
@@ -0,0 +1,99 @@
1
+ import { spawn, spawnSync } from 'child_process'
2
+ import { Command } from 'commander'
3
+ import { ensureDeployKey } from '../lib/deployKey'
4
+ import { PHIO_CONFIG_FILE } from '../lib/constants'
5
+ import { savedInstanceName } from '../lib/defaultInstanceId'
6
+ import { ensureLoggedIn } from '../lib/ensureLoggedIn'
7
+ import { getClient, getInstanceBySubdomainCnameOrId } from '../lib/getClient'
8
+ import {
9
+ formatSftpCommand,
10
+ PHIO_SFTP_HOST,
11
+ PHIO_SFTP_PORT,
12
+ type SftpConnection,
13
+ buildSftpArgs,
14
+ } from '../lib/sftpConnection'
15
+
16
+ const findSftpExecutable = (): string | null => {
17
+ const lookup = process.platform === 'win32' ? 'where' : 'which'
18
+ const result = spawnSync(lookup, ['sftp'], { encoding: 'utf8' })
19
+ if (result.status !== 0) {
20
+ return null
21
+ }
22
+ const line = result.stdout.trim().split(/\r?\n/)[0]?.trim()
23
+ return line || null
24
+ }
25
+
26
+ const resolveRemoteDir = async (instanceName?: string) => {
27
+ const name = instanceName || savedInstanceName()
28
+ if (!name) {
29
+ return ''
30
+ }
31
+
32
+ try {
33
+ const instance = await getInstanceBySubdomainCnameOrId(name)
34
+ return `${instance.subdomain}/`
35
+ } catch {
36
+ throw new Error(`Instance ${name} not found`)
37
+ }
38
+ }
39
+
40
+ export const openSftpSession = async (instanceName?: string, printOnly = false) => {
41
+ if (!instanceName && !savedInstanceName()) {
42
+ console.log(`No linked instance in ${PHIO_CONFIG_FILE}. Opening SFTP at instance root.`)
43
+ }
44
+
45
+ await ensureLoggedIn()
46
+ const client = await getClient()
47
+ const { privateKeyPath } = await ensureDeployKey(client)
48
+ const email = client.authStore.record?.email
49
+ if (!email) {
50
+ throw new Error(`You must be logged in first. Use 'phio login'`)
51
+ }
52
+
53
+ const connection: SftpConnection = {
54
+ host: PHIO_SFTP_HOST,
55
+ port: PHIO_SFTP_PORT,
56
+ username: email,
57
+ privateKeyPath,
58
+ remoteDir: await resolveRemoteDir(instanceName),
59
+ }
60
+
61
+ const commandLine = formatSftpCommand(connection)
62
+
63
+ if (printOnly) {
64
+ console.log(commandLine)
65
+ return
66
+ }
67
+
68
+ const sftpBin = findSftpExecutable()
69
+ if (!sftpBin) {
70
+ console.error(`Could not find 'sftp' on PATH. Install OpenSSH client tools, then run:`)
71
+ console.error('')
72
+ console.error(commandLine)
73
+ console.error('')
74
+ console.error('See https://pockethost.io/docs/ftp for client setup.')
75
+ process.exit(1)
76
+ }
77
+
78
+ const child = spawn(sftpBin, buildSftpArgs(connection), { stdio: 'inherit' })
79
+ await new Promise<void>((_resolve, reject) => {
80
+ child.on('error', reject)
81
+ child.on('close', (code, signal) => {
82
+ if (signal) {
83
+ process.kill(process.pid, signal)
84
+ return
85
+ }
86
+ process.exit(code ?? 0)
87
+ })
88
+ })
89
+ }
90
+
91
+ export const SftpCommand = () => {
92
+ return new Command('sftp')
93
+ .argument(`[instanceName]`, `Instance name`, savedInstanceName())
94
+ .description(`Open an interactive SFTP session to your instance files`)
95
+ .option(`--print`, `Print the sftp command instead of running it`)
96
+ .action(async (instanceName, options) => {
97
+ await openSftpSession(instanceName || undefined, options.print)
98
+ })
99
+ }
@@ -27,3 +27,5 @@ export const PHIO_USERNAME = () => env.get('PHIO_USERNAME').asString() || ''
27
27
  export const PHIO_PASSWORD = () => env.get('PHIO_PASSWORD').asString() || ''
28
28
  export const PHIO_INSTANCE_NAME = () =>
29
29
  env.get('PHIO_INSTANCE_NAME').asString() || ''
30
+
31
+ export const PHIO_CONFIG_FILE = '.phioconfig'