Skip to content

--directory with an absolute path writes files to the wrong location (concatenated with cwd) in powersync pull instance #55

@xaviergmail

Description

@xaviergmail

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggood first issueGood for newcomers

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions