Disclosure: This issue was researched, written, and filed by Claude (Anthropic's AI assistant) acting on behalf of an engineer at XOi Technologies. The reproduction, source-code analysis, and proposed fix below are Claude's work product; the human reviewer endorsed the diagnosis and asked Claude to open the issue upstream.
Summary
powersync pull instance --directory=<absolute-path> does not write files to <absolute-path>. Instead, files are written to join(process.cwd(), <absolute-path>), which is a concatenation rather than a replacement. The CLI's own log messages reference the user-supplied --directory value, so the bug is silent: the CLI reports success, the user looks at the path they passed, finds nothing, and assumes the files were deleted or never written.
This affects (at least) powersync@0.9.3 and was reproduced against the current main. Other commands that go through resolveProjectDir likely have the same bug.
Reproduction
$ npm i -g powersync@0.9.3
$ cd ~
$ powersync pull instance \
--project-id=<PROJ_ID> \
--instance-id=<INST_ID> \
--directory=/tmp/powersync-repro \
--overwrite
Created /tmp/powersync-repro/cli.yaml with Cloud instance link.
Fetching config for instance ... in project ... in org ...
Wrote service.yaml with config from the cloud.
Wrote sync-config.yaml with sync config from the cloud.
$ ls /tmp/powersync-repro/
ls: cannot access '/tmp/powersync-repro/': No such file or directory
$ ls ~/tmp/powersync-repro/
cli.yaml service.yaml sync-config.yaml
The CLI says it wrote to /tmp/powersync-repro/. The files are actually at $HOME/tmp/powersync-repro/.
Where it bit us
We use powersync pull instance in a GitHub Actions workflow to validate that this repo's schema.sql matches the deployed PowerSync sync rules. The workflow does:
- name: Fetch deployed sync rules
run: |
mkdir -p /tmp/powersync
powersync pull instance \
--project-id="$PS_PROJECT_ID" \
--instance-id="$PS_INSTANCE_ID" \
--directory=/tmp/powersync \
--overwrite
test -f /tmp/powersync/sync-config.yaml # fails — file isn't there
The CLI logs every "Wrote …" success message, exits 0, and then our test -f fails because the files are actually at $GITHUB_WORKSPACE/tmp/powersync/. The exit-0 success makes this look like a downstream pipeline bug rather than a CLI bug.
Root cause
packages/cli-core/src/command-types/PowerSyncCommand.ts:
resolveProjectDir(flags: { directory: string }): string {
return join(process.cwd(), flags.directory);
}
Node's path.join does not treat an absolute second argument specially — it simply concatenates:
> require('path').join('/foo/bar', '/tmp/x')
'/foo/bar/tmp/x'
> require('path').resolve('/foo/bar', '/tmp/x')
'/tmp/x'
(path.resolve does the right thing: an absolute argument discards everything to its left.)
In cli/src/commands/pull/instance.ts the user-facing log messages use the raw directory flag while the file writes use the resolved projectDir, so the bug is silent:
const projectDir = this.resolveProjectDir(flags); // /cwd/tmp/powersync — WRONG
// ...
writeCloudLink(projectDir, { ... });
this.log(`Created ${ux.colorize('blue', `${directory}/${CLI_FILENAME}`)} ...`); // logs /tmp/powersync — user-supplied value
// ...
const serviceOutputPath = join(projectDir, serviceOutputName); // /cwd/tmp/powersync/service.yaml
writeFileSync(serviceOutputPath, serviceYaml, 'utf8');
this.log(`Wrote ${ux.colorize('blue', serviceOutputName)} ...`); // logs just the filename
Expected behavior
--directory=/tmp/powersync writes files to /tmp/powersync/, regardless of the process's current working directory.
Suggested fix
Two equivalent one-line changes:
Option A — use path.resolve:
import { resolve } from 'node:path';
resolveProjectDir(flags: { directory: string }): string {
return resolve(process.cwd(), flags.directory);
}
Option B — guard against absolute input explicitly:
import { isAbsolute, join } from 'node:path';
resolveProjectDir(flags: { directory: string }): string {
return isAbsolute(flags.directory) ? flags.directory : join(process.cwd(), flags.directory);
}
Either fix preserves existing behavior for relative --directory values (the common case) and correctly honors absolute paths.
A secondary cleanup: have the user-facing this.log(…) messages in pull/instance.ts reference the resolved projectDir instead of the raw directory flag so future divergences between the two are immediately visible.
Workaround
Pass a relative --directory and cd (or working-directory: in Actions) into the parent first:
- name: Fetch deployed sync rules
working-directory: /tmp
run: |
mkdir -p powersync
powersync pull instance \
--project-id="$PS_PROJECT_ID" \
--instance-id="$PS_INSTANCE_ID" \
--directory=powersync \
--overwrite
Environment
powersync@0.9.3 (Node 20.x, Ubuntu 24.04 GitHub Actions runner; also reproduced locally on macOS).
- Source analysis matches current
main at the time of filing.
Other commands likely affected
Anything that goes through resolveProjectDir. From a quick grep:
cli/src/commands/pull/instance.ts
cli/src/commands/link/self-hosted.ts
cli/src/commands/init/cloud.ts
packages/cli-core/src/command-types/InstanceCommand.ts
Happy to test a fix or open a PR if it'd help — let me know.
Summary
powersync pull instance --directory=<absolute-path>does not write files to<absolute-path>. Instead, files are written tojoin(process.cwd(), <absolute-path>), which is a concatenation rather than a replacement. The CLI's own log messages reference the user-supplied--directoryvalue, so the bug is silent: the CLI reports success, the user looks at the path they passed, finds nothing, and assumes the files were deleted or never written.This affects (at least)
powersync@0.9.3and was reproduced against the currentmain. Other commands that go throughresolveProjectDirlikely have the same bug.Reproduction
The CLI says it wrote to
/tmp/powersync-repro/. The files are actually at$HOME/tmp/powersync-repro/.Where it bit us
We use
powersync pull instancein a GitHub Actions workflow to validate that this repo'sschema.sqlmatches the deployed PowerSync sync rules. The workflow does:The CLI logs every "Wrote …" success message, exits 0, and then our
test -ffails because the files are actually at$GITHUB_WORKSPACE/tmp/powersync/. The exit-0 success makes this look like a downstream pipeline bug rather than a CLI bug.Root cause
packages/cli-core/src/command-types/PowerSyncCommand.ts:Node's
path.joindoes not treat an absolute second argument specially — it simply concatenates:(
path.resolvedoes the right thing: an absolute argument discards everything to its left.)In
cli/src/commands/pull/instance.tsthe user-facing log messages use the rawdirectoryflag while the file writes use the resolvedprojectDir, so the bug is silent:Expected behavior
--directory=/tmp/powersyncwrites files to/tmp/powersync/, regardless of the process's current working directory.Suggested fix
Two equivalent one-line changes:
Option A — use
path.resolve:Option B — guard against absolute input explicitly:
Either fix preserves existing behavior for relative
--directoryvalues (the common case) and correctly honors absolute paths.A secondary cleanup: have the user-facing
this.log(…)messages inpull/instance.tsreference the resolvedprojectDirinstead of the rawdirectoryflag so future divergences between the two are immediately visible.Workaround
Pass a relative
--directoryandcd(orworking-directory:in Actions) into the parent first:Environment
powersync@0.9.3(Node 20.x, Ubuntu 24.04 GitHub Actions runner; also reproduced locally on macOS).mainat the time of filing.Other commands likely affected
Anything that goes through
resolveProjectDir. From a quick grep:cli/src/commands/pull/instance.tscli/src/commands/link/self-hosted.tscli/src/commands/init/cloud.tspackages/cli-core/src/command-types/InstanceCommand.tsHappy to test a fix or open a PR if it'd help — let me know.