Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
136 commits
Select commit Hold shift + click to select a range
ed348e3
basic scaffolding
stevensJourney Feb 2, 2026
2e6fa58
confg templates
stevensJourney Feb 3, 2026
73763b2
Schema packages. Improve templates. WIP link commands.
stevensJourney Feb 3, 2026
f8021ca
wip: linking
stevensJourney Feb 3, 2026
16352ad
unify config types
stevensJourney Feb 3, 2026
63eebde
Command structures and implement deploy command
stevensJourney Feb 4, 2026
ec8d323
Add destroy and stop command implementation
stevensJourney Feb 4, 2026
77cc382
fetch and pull cloud config commands
stevensJourney Feb 4, 2026
a066fd7
Improvements from testing deploy workflow
stevensJourney Feb 5, 2026
0def649
Add multiple options for specifying cloud instance
stevensJourney Feb 5, 2026
23ad67e
Various command improvements. cloud Token and schema generation comma…
stevensJourney Feb 5, 2026
5d9a090
wip: shared commands
stevensJourney Feb 6, 2026
14d3e51
Help groups for CLI command flags. Unify linking resolve patterns. Up…
stevensJourney Feb 6, 2026
51bd353
validation commands
stevensJourney Feb 6, 2026
5a916c1
update docs
stevensJourney Feb 6, 2026
874e4d4
Support for naming and creating cloud instances
stevensJourney Feb 6, 2026
78fcb42
cleanup token input
stevensJourney Feb 6, 2026
321591c
update commands' descriptions
stevensJourney Feb 6, 2026
4d76397
strategic command shuffle
stevensJourney Feb 6, 2026
873d463
cli plugin framework. Docker deploy for self hosting example plugin.
stevensJourney Feb 6, 2026
a13c9f8
monorepo structure
stevensJourney Feb 6, 2026
69c3250
add usage docs for docker
stevensJourney Feb 6, 2026
5ec0fe3
wip: update docker templates
stevensJourney Feb 9, 2026
8a70580
docker plugin templating improvements
stevensJourney Feb 9, 2026
40a92b4
update workspace layout
stevensJourney Feb 9, 2026
78cc6d3
cleanup templates
stevensJourney Feb 9, 2026
35d2983
update readme
stevensJourney Feb 9, 2026
2798d21
formatting
stevensJourney Feb 9, 2026
68104f7
cleanup cloud commands
stevensJourney Feb 9, 2026
ec8180f
add colour to logs
stevensJourney Feb 9, 2026
756db05
update packages
stevensJourney Feb 9, 2026
90164bc
update start command docs
stevensJourney Feb 9, 2026
e6df80f
colourize errors. Add some basic examples
stevensJourney Feb 10, 2026
deface8
pretty format client errors
stevensJourney Feb 10, 2026
4ed581b
fix cloud deploy command failure. use micro web sdk client.
stevensJourney Feb 11, 2026
78f1c48
styled errors
stevensJourney Feb 11, 2026
bd40e12
cleanup logs - format command strings to standout
stevensJourney Feb 11, 2026
f8ed092
update fetch status command
stevensJourney Feb 11, 2026
b6face2
update examples with readmes
stevensJourney Feb 11, 2026
7c13bdc
Add basic node.js example app
stevensJourney Feb 11, 2026
a7a91fc
update docs
stevensJourney Feb 11, 2026
885dd0b
share code with core-api
stevensJourney Feb 11, 2026
604cab3
use relative paths for Docker
stevensJourney Feb 11, 2026
34e0ade
specify docker project name in compose projects
stevensJourney Feb 11, 2026
59edbca
accounts client and list instances command
stevensJourney Feb 12, 2026
5030b44
cleanup project loading
stevensJourney Feb 12, 2026
30720bb
cleanup token storage. Improve login command.
stevensJourney Feb 12, 2026
e517f95
wip: browser based PAT for login
stevensJourney Feb 16, 2026
cc979fa
improve config pull output
stevensJourney Feb 16, 2026
91e7667
improve schemas and config validation
stevensJourney Feb 16, 2026
758cf7e
update readmes
stevensJourney Feb 16, 2026
5781534
split init commands. Add interactivity to docker configure
stevensJourney Feb 16, 2026
b7aee18
refactor: rename link.yaml to cli.yaml
stevensJourney Feb 16, 2026
aef9ced
clean login command
stevensJourney Feb 16, 2026
4506cdc
Add ability to configure custom YAML tags for VSCode.
stevensJourney Feb 16, 2026
89aeec9
show progress during validations. neaten init commad logs.
stevensJourney Feb 17, 2026
25faae9
add more usage docs. refactor command name to powersync pull instance
stevensJourney Feb 17, 2026
c3a19f4
default org-id for PAT tokens with single org
stevensJourney Feb 17, 2026
e8aea02
readme cleanup
stevensJourney Feb 17, 2026
fe892ac
fallback login token to prompt if dashboard cannot send it
stevensJourney Feb 17, 2026
66a7fb5
improve PAT login with encryption for edge cases.
stevensJourney Feb 18, 2026
3d1a218
respond with html for long
stevensJourney Feb 19, 2026
4de930f
feat: migration of sync rules
stevensJourney Feb 19, 2026
ef7bac7
remove service-schema override
stevensJourney Feb 19, 2026
22d410c
less colour is more colour
stevensJourney Feb 19, 2026
0c60b99
rename sync rules to sync config. Add command for only deploying sync…
stevensJourney Feb 19, 2026
e082df0
YAML language server templating
stevensJourney Feb 19, 2026
23aff77
More token storage options.
stevensJourney Feb 19, 2026
a239df9
minor log cleanup
stevensJourney Feb 20, 2026
c837230
remove pnpm overrides
stevensJourney Feb 20, 2026
9d1a14d
formatting
stevensJourney Feb 20, 2026
1cc41b8
use single pnpm version
stevensJourney Feb 20, 2026
50d9ef1
remove local override
stevensJourney Feb 20, 2026
ae2f997
fix prettier ignore
stevensJourney Feb 20, 2026
770426c
pretty lint
stevensJourney Feb 20, 2026
6d1cd26
more cleanup
stevensJourney Feb 20, 2026
dce470e
cleanup
stevensJourney Feb 20, 2026
9e54182
try fix windows
stevensJourney Feb 20, 2026
39ac48a
tests for login command
stevensJourney Feb 20, 2026
bcf14bc
Add example commands
stevensJourney Feb 20, 2026
a2929c0
cleanup template config
stevensJourney Feb 20, 2026
9fd74e8
Merge branch 'init' into sync-rules-migrate
stevensJourney Feb 20, 2026
d61e2bc
update sync stream defaults
stevensJourney Feb 20, 2026
921e61b
formatting
stevensJourney Feb 20, 2026
054fadf
lint
stevensJourney Feb 20, 2026
50a45f6
update readme
stevensJourney Feb 20, 2026
caebe3f
Update cli/src/api/write-vscode-settings-for-yaml-env.ts
stevensJourney Feb 23, 2026
dccb61d
pr feedback items
stevensJourney Feb 23, 2026
d6f249f
Merge branch 'init' of github.com:powersync-ja/temp-cli into init
stevensJourney Feb 23, 2026
8429145
lint
stevensJourney Feb 23, 2026
2f6af9f
cleanup
stevensJourney Feb 23, 2026
f86df4e
ensure directory is required for multiple directories
stevensJourney Feb 23, 2026
e16a941
update sync config examples to edition 3
stevensJourney Feb 23, 2026
e729d8e
remove debug_api from templates
stevensJourney Feb 23, 2026
4c5bbaf
list is_provisioned in instance statuses. Wait for destroy and stop c…
stevensJourney Feb 24, 2026
34cef60
document oclif plugins
stevensJourney Feb 24, 2026
1c140ba
update templates in order to create cloud instances without replicati…
stevensJourney Feb 24, 2026
9a4bcd1
update instance status data_queries to used_for_replication
stevensJourney Feb 24, 2026
7cfd2e9
update validate message.
stevensJourney Feb 24, 2026
89385b8
update validation messages. refresh unit tests
stevensJourney Feb 24, 2026
4a5d932
fix tests
stevensJourney Feb 24, 2026
8665efe
husky lint
stevensJourney Feb 24, 2026
23d5999
more husky
stevensJourney Feb 24, 2026
3c4329d
update docs to refer to powersync docker command and topic - not plugin
stevensJourney Feb 24, 2026
78c4f15
wip: cleanup commands and topics
stevensJourney Feb 24, 2026
a6c1f0a
add autocomplete
stevensJourney Feb 24, 2026
1ba7179
add commands plugin
stevensJourney Feb 25, 2026
c35a15d
rename sync.yaml to sync-config.yaml
stevensJourney Feb 25, 2026
d952cca
update package name
stevensJourney Feb 25, 2026
268e879
add install instructions to readme
stevensJourney Feb 25, 2026
2188bef
update build script
stevensJourney Feb 25, 2026
11eb305
fix login command when pasting a PAT
stevensJourney Feb 25, 2026
b88dbe6
cleanup flag resolution order
stevensJourney Feb 25, 2026
80560f9
cleanup login command
stevensJourney Feb 25, 2026
d33535d
add ability to deploy only service config changes (no sync config)
stevensJourney Feb 25, 2026
7e1c365
cleanup deploy logging
stevensJourney Feb 25, 2026
4844ba2
Polish the yaml templates (#5)
benitav Feb 25, 2026
fe6fc60
fix format
stevensJourney Feb 25, 2026
38d88b5
Merge branch 'init' of github.com:powersync-ja/temp-cli into init
stevensJourney Feb 25, 2026
04019b7
fix tests
stevensJourney Feb 25, 2026
72f6b7f
fix typo
stevensJourney Feb 25, 2026
7f986e7
add note about cloud secrets
stevensJourney Feb 26, 2026
e29b1c5
allow linking if project directory does not exist
stevensJourney Feb 26, 2026
58d23b7
add powersync status alias
stevensJourney Feb 26, 2026
64e25ec
add objectId validation, and org - project existance checks
stevensJourney Feb 26, 2026
0653202
remove unknown error code.
stevensJourney Feb 26, 2026
1d87c0d
rename TOKEN to PS_ADMIN_TOKEN
stevensJourney Feb 26, 2026
9ff2af3
fix format
stevensJourney Feb 26, 2026
05177fb
assign project for self hosted commands
stevensJourney Feb 26, 2026
f740284
feat: add user agent
stevensJourney Feb 26, 2026
bab64b9
Merge branch 'main' into user-agent
stevensJourney Mar 2, 2026
2d6c20b
fix merge conflicts
stevensJourney Mar 2, 2026
4e3dcf0
fix: remove stale lowercase services.ts from git index
stevensJourney Mar 2, 2026
46a24a1
add unit test. move header store to shared file
stevensJourney Mar 2, 2026
410d833
update tests
stevensJourney Mar 2, 2026
05f40e4
mock auth
stevensJourney Mar 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cli/bin/dev.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
#!/usr/bin/env -S node --import tsx

import { execute } from '@oclif/core';
import { setCliClientHeaders } from '@powersync/cli-core';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import packageJSON from '../package.json' with { type: 'json' };

setCliClientHeaders({
'user-agent': `POWERSYNC_CLI/${packageJSON.version}`
});

const __dirname = path.dirname(fileURLToPath(import.meta.url));

await execute({
Expand Down
7 changes: 7 additions & 0 deletions cli/bin/run.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
#!/usr/bin/env node

import { execute } from '@oclif/core';
import { setCliClientHeaders } from '@powersync/cli-core';

import packageJSON from '../package.json' with { type: 'json' };

setCliClientHeaders({
'user-agent': `POWERSYNC_CLI/${packageJSON.version}`
});

await execute({ dir: import.meta.url });
2 changes: 1 addition & 1 deletion cli/src/api/cloud/validate-cloud-link-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export async function validateCloudLinkConfig(
ensureObjectId(orgId, '--org-id');
ensureObjectId(projectId, '--project-id');

const accountsClient = await createAccountsHubClient();
const accountsClient = createAccountsHubClient();

try {
await accountsClient.getOrganization({ id: orgId });
Expand Down
4 changes: 2 additions & 2 deletions cli/src/commands/fetch/instances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ export default class FetchInstances extends Command {
static summary = '[Cloud only] List Cloud instances in the current org/project.';

async run(): Promise<void> {
const accountsClient = await createAccountsHubClient();
const managementClient = await createCloudClient();
const accountsClient = createAccountsHubClient();
const managementClient = createCloudClient();

const { flags } = await this.parse(FetchInstances);
const { org_id, project_id } = flags;
Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/generate/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default class GenerateToken extends SharedInstanceCommand {

protected async generateCloudToken(project: CloudProject, config: TokenConfig): Promise<string> {
const { linked } = project;
const client = await createCloudClient();
const client = createCloudClient();

// Get the config in order to check if development tokens are enabled.
const cloudInstanceConfig = await client
Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default class Login extends PowerSyncCommand {
}

const listOrgs = async (): Promise<string> => {
const accountsHubClient = await createAccountsHubClient();
const accountsHubClient = createAccountsHubClient();
const orgs = await accountsHubClient.listOrganizations({});
const objects = orgs?.objects ?? [];
return objects.map((org) => `\t - ${org.label} - ${org.id}`).join('\n');
Expand Down
70 changes: 70 additions & 0 deletions cli/test/clients/cli-client-headers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
createAccountsHubClient,
createSelfHostedClient,
env,
Services,
setCliClientHeaders
} from '@powersync/cli-core';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';

describe('cli client headers', () => {
let fetchSpy: ReturnType<typeof vi.spyOn>;

beforeEach(async () => {
vi.resetModules();

// Ensure the accounts client has a token available.
env.PS_ADMIN_TOKEN = 'test-token';
vi.spyOn(Services.authentication, 'getToken').mockResolvedValue('test-token');

// Spy on fetch with endpoint-specific responses so SDK calls settle.
fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const url = typeof input === 'string' ? input : input.toString();

if (url.includes('/api/admin/v1/diagnostics')) {
return new Response(JSON.stringify({ data: { connections: [] } }), {
headers: { 'content-type': 'application/json' },
status: 200
});
}

if (url.includes('/api/accounts/v5/organizations/get')) {
return new Response(JSON.stringify({ id: 'org', label: 'test' }), {
headers: { 'content-type': 'application/json' },
status: 200
});
}

return new Response('{}', {
headers: { 'content-type': 'application/json' },
status: 200
});
});
});

afterEach(() => {
vi.restoreAllMocks();
env.PS_ADMIN_TOKEN = undefined;
});

test('applies CLI headers across self-hosted, accounts, and cloud clients', async () => {
setCliClientHeaders({ 'user-agent': 'POWERSYNC_CLI/test', 'x-custom': 'value' });

// We don't use a cloud client directly, since that is mocked other tests' convinience.
const selfHosted = createSelfHostedClient({ apiKey: 'key', apiUrl: 'test-url' });
const accounts = createAccountsHubClient();

await Promise.all([selfHosted.diagnostics({}), accounts.getOrganization({ id: 'org' })]);

expect(fetchSpy).toHaveBeenCalledTimes(2);

const headerSets: Headers[] = fetchSpy.mock.calls.map(
(args: [unknown, { headers?: Record<string, string> }?]) => new Headers(args[1]?.headers)
);

for (const headers of headerSets) {
expect(headers.get('user-agent')).toEqual('POWERSYNC_CLI/test');
expect(headers.get('x-custom')).toEqual('value');
}
});
});
4 changes: 2 additions & 2 deletions cli/test/commands/link.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ const accountsClientMock = {
listProjects: vi.fn()
};

vi.spyOn(cliCore, 'createAccountsHubClient').mockResolvedValue(
accountsClientMock as unknown as Awaited<ReturnType<typeof cliCore.createAccountsHubClient>>
vi.spyOn(cliCore, 'createAccountsHubClient').mockImplementation(
() => accountsClientMock as unknown as ReturnType<typeof cliCore.createAccountsHubClient>
);

function writeServiceYaml(projectDir: string, type: 'cloud' | 'self-hosted') {
Expand Down
2 changes: 1 addition & 1 deletion cli/test/commands/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe('login', () => {
address: 'http://127.0.0.1:54321',
tokenPromise: Promise.resolve('server-token')
});
mockedCreateAccountsHubClient.mockResolvedValue({
mockedCreateAccountsHubClient.mockReturnValue({
listOrganizations: vi.fn().mockResolvedValue({
objects: [{ id: 'org-1', label: 'Org One' }]
})
Expand Down
2 changes: 1 addition & 1 deletion cli/test/commands/pull/instance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const accountsClientMock = {
};

vi.spyOn(cliCore, 'createAccountsHubClient').mockImplementation(
async () => accountsClientMock as unknown as Awaited<ReturnType<typeof cliCore.createAccountsHubClient>>
() => accountsClientMock as unknown as ReturnType<typeof cliCore.createAccountsHubClient>
);

function writeServiceYaml(projectDir: string, type: 'cloud' | 'self-hosted') {
Expand Down
30 changes: 17 additions & 13 deletions packages/cli-core/src/clients/AccountsHubClientSDKClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ux } from '@oclif/core';

import { Services } from '../services/Services.js';
import { env } from '../utils/env.js';
import { getCliClientHeadersStore } from './cli-client-headers.js';

/**
* Client for interacting with the AccountsHub API service.
Expand Down Expand Up @@ -59,20 +60,23 @@ export class AccountsHubClientSDKClient<C extends sdk.NetworkClient = sdk.Networ
* Creates a PowerSync Accounts Hub Client for the Cloud.
* Uses the token stored by the login command (secure storage, e.g. macOS Keychain).
*/
export async function createAccountsHubClient(): Promise<AccountsHubClientSDKClient> {
const { authentication } = Services;
const token = env.PS_ADMIN_TOKEN || (await authentication.getToken());
if (!token) {
throw new Error(
`Not logged in. Run ${ux.colorize('blue', 'powersync login')} to authenticate (you will be prompted for your token), or provide the ${ux.colorize('blue', 'PS_ADMIN_TOKEN')} environment variable.`
);
}

export function createAccountsHubClient(): AccountsHubClientSDKClient {
return new AccountsHubClientSDKClient({
client: sdk.createWebNetworkClient({
headers: () => ({
Authorization: `Bearer ${token}`
})
async headers() {
const { authentication } = Services;
const token = env.PS_ADMIN_TOKEN || (await authentication.getToken());
if (!token) {
throw new Error(
`Not logged in. Run ${ux.colorize('blue', 'powersync login')} to authenticate (you will be prompted for your token), or provide the ${ux.colorize('blue', 'PS_ADMIN_TOKEN')} environment variable.`
);
}

return {
...getCliClientHeadersStore().headers,
Authorization: `Bearer ${token}`
};
}
}),
endpoint: env._PS_ACCOUNTS_HUB_SERVICE_URL
});
Expand All @@ -86,7 +90,7 @@ export async function createAccountsHubClient(): Promise<AccountsHubClientSDKCli
* @throws If the token has zero or multiple orgs (caller should ask the user to pass --org-id).
*/
export async function getDefaultOrgId(): Promise<string> {
const client = await createAccountsHubClient();
const client = createAccountsHubClient();
const { objects: organizations, total } = await client.listOrganizations({});
if (total === 0) {
throw new Error(
Expand Down
38 changes: 38 additions & 0 deletions packages/cli-core/src/clients/cli-client-headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Process-wide store key for CLI client headers.
*
* Why global:
* - CLI startup code lives in the `cli` package, while client creation lives in `cli-core`.
* - In some environments, duplicate copies of `@powersync/cli-core` could be loaded (in the future perhaps).
* - Module-level state would then be duplicated, causing header injection (for example User-Agent)
* to only affect one copy.
*
* Using `globalThis` + `Symbol.for(...)` gives us one shared store for this process,
* regardless of how many module instances are loaded.
*/
const CLI_CLIENT_HEADERS_STORE_KEY = Symbol.for('powersync.cli-core.cliClientHeaders');

type CliClientHeadersStore = {
headers: Record<string, string>;
};

export function getCliClientHeadersStore(): CliClientHeadersStore {
// Read/write the shared process-wide store so all cli-core instances observe the same headers.
const globalScope = globalThis as typeof globalThis & {
[CLI_CLIENT_HEADERS_STORE_KEY]?: CliClientHeadersStore;
};

if (!globalScope[CLI_CLIENT_HEADERS_STORE_KEY]) {
globalScope[CLI_CLIENT_HEADERS_STORE_KEY] = { headers: {} };
}

return globalScope[CLI_CLIENT_HEADERS_STORE_KEY];
}

/**
* Sets headers that are applied to all outbound CLI clients (cloud and self-hosted).
* Existing clients also pick up updates because headers are resolved per request.
*/
export function setCliClientHeaders(headers: Record<string, string>): void {
Object.assign(getCliClientHeadersStore().headers, headers);
}
2 changes: 2 additions & 0 deletions packages/cli-core/src/clients/create-cloud-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { PowerSyncManagementClient } from '@powersync/management-client';

import { Services } from '../services/Services.js';
import { env } from '../utils/env.js';
import { getCliClientHeadersStore } from './cli-client-headers.js';

/**
* Creates a PowerSync Management Client for the Cloud.
Expand All @@ -30,6 +31,7 @@ export function createCloudClient(): PowerSyncManagementClient {
}

return {
...getCliClientHeadersStore().headers,
Authorization: `Bearer ${token}`
};
}
Expand Down
9 changes: 8 additions & 1 deletion packages/cli-core/src/clients/create-self-hosted-client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as sdk from '@journeyapps-labs/common-sdk';
import { InstanceClient } from '@powersync/service-client';

import { getCliClientHeadersStore } from './cli-client-headers.js';

export type SelfHostedClientConfig = {
apiKey: string;
apiUrl: string;
Expand All @@ -11,8 +13,13 @@ export type SelfHostedClientConfig = {
*/
export function createSelfHostedClient(config: SelfHostedClientConfig) {
return new InstanceClient({
client: sdk.createNodeNetworkClient({
/**
* Use the web (fetch-based) network client to mirror the cloud client behavior and
* allow fetch to be spied on in tests. Node exposes fetch globally so we can rely on it.
*/
client: sdk.createWebNetworkClient({
headers: () => ({
...getCliClientHeadersStore().headers,
Authorization: `Bearer ${config.apiKey}`
})
}),
Expand Down
1 change: 1 addition & 0 deletions packages/cli-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Plugins (e.g. plugin-docker) import from @powersync/cli-core.
*/
export * from './clients/AccountsHubClientSDKClient.js';
export * from './clients/cli-client-headers.js';
export * from './clients/create-cloud-client.js';
export * from './clients/create-self-hosted-client.js';
export * from './command-types/CloudInstanceCommand.js';
Expand Down