Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
cacd241
feat: skip sync validation
stevensJourney Mar 5, 2026
be56883
cleanup log order
stevensJourney Mar 6, 2026
0ebd3ec
fix lockfile
stevensJourney Mar 6, 2026
e5bf281
Merge branch 'main' into skip-sync-validation
stevensJourney Mar 16, 2026
3aad6dc
fix merge conflicts
stevensJourney Mar 16, 2026
361f76c
minor renames
stevensJourney Mar 16, 2026
8f92b7e
move
stevensJourney Mar 16, 2026
f471eac
validations move
stevensJourney Mar 16, 2026
74397ba
allow specifying which validations should be executed for all validat…
stevensJourney Mar 16, 2026
6a29e90
stablize test names. add unit tests
stevensJourney Mar 17, 2026
4baa89e
Merge remote-tracking branch 'origin/main' into skip-sync-validation
stevensJourney Mar 17, 2026
5ff5b14
cleanup and add changeset
stevensJourney Mar 17, 2026
cc201e8
cleanup
stevensJourney Mar 17, 2026
2b23702
add workaround for race condition when reprovisioning
stevensJourney Mar 17, 2026
767b11a
wip: override sync config path
stevensJourney Mar 18, 2026
9fddc93
Actually allow deploys to attempt deploying config which does not mat…
stevensJourney Mar 18, 2026
56f6a95
Merge branch 'skip-sync-validation' into configurable-sync-config-param
stevensJourney Mar 18, 2026
7e7690e
feat: ability to override sync config filename
stevensJourney Mar 18, 2026
08112d0
Merge remote-tracking branch 'origin/main' into configurable-sync-con…
stevensJourney Mar 18, 2026
88f6693
cleanup tests
stevensJourney Mar 18, 2026
da7160b
add changeset
stevensJourney Mar 18, 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
5 changes: 5 additions & 0 deletions .changeset/nice-pears-find.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'powersync': patch
---

Added `--sync-config-file-path` so local sync YAML can be read from a path other than `sync-config.yaml` for **`powersync deploy`**, **`powersync deploy sync-config`**, **`powersync validate`**, and **`powersync generate schema`**.
4 changes: 2 additions & 2 deletions cli/src/api/BaseDeployCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { DEFAULT_DEPLOY_TIMEOUT_MS, waitForOperationStatusChange } from './cloud
import { parseLocalCloudServiceConfig } from './parse-local-cloud-service-config.js';

export default abstract class BaseDeployCommand extends CloudInstanceCommand {
static flags = {
static baseFlags = {
'deploy-timeout': Flags.integer({
default: DEFAULT_DEPLOY_TIMEOUT_MS / 1000,
description:
Expand All @@ -26,7 +26,7 @@ export default abstract class BaseDeployCommand extends CloudInstanceCommand {
return value;
}
}),
...CloudInstanceCommand.flags
...CloudInstanceCommand.baseFlags
};

protected async deployAll(params: {
Expand Down
14 changes: 6 additions & 8 deletions cli/src/api/cloud/fetch-cloud-sync-rules-content.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import { CloudProject, createCloudClient, SYNC_FILENAME } from '@powersync/cli-core';
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { CloudProject, createCloudClient } from '@powersync/cli-core';

/**
* Fetches the sync config content for a cloud project.
* @param project - The project to fetch the sync config content for.
* @returns The sync config content.
*/
export async function fetchCloudSyncRulesContent(project: CloudProject): Promise<string> {
const { linked } = project;
const client = await createCloudClient();

// First try and use the local file
if (existsSync(join(project.projectDirectory, SYNC_FILENAME))) {
return readFileSync(join(project.projectDirectory, SYNC_FILENAME), 'utf8');
if (project.syncRulesContent) {
return project.syncRulesContent;
}

const { linked } = project;
const client = createCloudClient();

// Try and fetch from the cloud config
const instanceConfig = await client.getInstanceConfig({
app_id: linked.project_id,
Expand Down
16 changes: 7 additions & 9 deletions cli/src/api/self-hosted/fetch-self-hosted-sync-rules-content.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
import { createSelfHostedClient, SelfHostedProject, SYNC_FILENAME } from '@powersync/cli-core';
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { createSelfHostedClient, SelfHostedProject } from '@powersync/cli-core';

/**
* Fetches the sync config content for a self-hosted project.
* @param project - The project to fetch the sync config content for.
* @returns The sync config content.
*/
export async function fetchSelfHostedSyncRulesContent(project: SelfHostedProject): Promise<string> {
// First try and use the local file
if (project.syncRulesContent) {
return project.syncRulesContent;
}

const { linked } = project;
const client = createSelfHostedClient({
apiKey: linked.api_key,
apiUrl: linked.api_url
});

// First try and use the local file
if (existsSync(join(project.projectDirectory, SYNC_FILENAME))) {
return readFileSync(join(project.projectDirectory, SYNC_FILENAME), 'utf8');
}

// Try and fetch from the cloud config
// Try and fetch from the self-hosted config
const instanceConfig = await client.diagnostics({});
const content = instanceConfig.active_sync_rules?.content;

Expand Down
8 changes: 3 additions & 5 deletions cli/src/api/validations/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,14 @@ export async function runSyncConfigTestCloud(project: CloudProject): Promise<Syn
* Runs self-hosted sync-rules validation and maps diagnostics into warning/error message arrays.
*/
export async function runSyncConfigTestSelfHosted(project: SelfHostedProject): Promise<SyncValidationTestRunResult> {
const syncRulesPath = join(project.projectDirectory, SYNC_FILENAME);
const syncRulesContent = existsSync(syncRulesPath) ? readFileSync(syncRulesPath, 'utf8') : undefined;
const syncText = syncRulesContent ?? '';
const syncRulesContent = project.syncRulesContent ?? '';
try {
return wrapsSyncValidation({
result: await validateSelfHostedSyncRules({
linked: project.linked,
syncRulesContent: syncText
syncRulesContent
}),
syncText
syncText: syncRulesContent
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
Expand Down
6 changes: 2 additions & 4 deletions cli/src/commands/deploy/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ux } from '@oclif/core';
import { WithSyncConfigFilePath } from '@powersync/cli-core';

import BaseDeployCommand from '../../api/BaseDeployCommand.js';
import { DEFAULT_DEPLOY_TIMEOUT_MS } from '../../api/cloud/wait-for-operation.js';
Expand All @@ -8,7 +9,7 @@ import { formatValidationHuman } from '../../api/validations/validation-utils.js
import { ValidationsRunner } from '../../api/validations/ValidationsRunner.js';
import { ValidationTest } from '../../api/validations/ValidationTestDefinition.js';

export default class DeployAll extends BaseDeployCommand {
export default class DeployAll extends WithSyncConfigFilePath(BaseDeployCommand) {
static description = [
'Deploy local config (service.yaml, sync config) to the linked PowerSync Cloud instance.',
'Validates connections and sync config before deploying.',
Expand All @@ -20,7 +21,6 @@ export default class DeployAll extends BaseDeployCommand {
'<%= config.bin %> <%= command.id %> --instance-id=<id> --project-id=<id>'
];
static flags = {
...BaseDeployCommand.flags,
...GENERAL_VALIDATION_FLAG_HELPERS.flags
};
static summary = '[Cloud only] Deploy local config to the linked Cloud instance (connections + auth + sync config).';
Expand All @@ -29,15 +29,13 @@ export default class DeployAll extends BaseDeployCommand {
const { flags } = await this.parse(DeployAll);

const project = await this.loadProject(flags, {
// local config state is required when deploying all
configFileRequired: true
});

const deployTimeoutMs = (flags['deploy-timeout'] ?? DEFAULT_DEPLOY_TIMEOUT_MS / 1000) * 1000;

const validationTestsFilter = GENERAL_VALIDATION_FLAG_HELPERS.parseValidationTestFlags(flags);

// The existing config is required to deploy changes. The instance should have been created already.
const cloudConfigState = await this.loadCloudConfigState();

// Parse and store for later
Expand Down
3 changes: 1 addition & 2 deletions cli/src/commands/deploy/service-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ export default class DeployServiceConfig extends BaseDeployCommand {
'<%= config.bin %> <%= command.id %> --instance-id=<id> --project-id=<id>'
];
static flags = {
...SERVICE_CONFIG_VALIDATION_FLAGS.flags,
...BaseDeployCommand.flags
...SERVICE_CONFIG_VALIDATION_FLAGS.flags
};
static summary = '[Cloud only] Deploy only local service config to the linked Cloud instance.';

Expand Down
20 changes: 7 additions & 13 deletions cli/src/commands/deploy/sync-config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Flags } from '@oclif/core';
import { ux } from '@oclif/core/ux';
import { WithSyncConfigFilePath } from '@powersync/cli-core';
import { routes } from '@powersync/management-types';
import { ObjectId } from 'bson';
import { readFileSync } from 'node:fs';

import BaseDeployCommand from '../../api/BaseDeployCommand.js';
import { DEFAULT_DEPLOY_TIMEOUT_MS } from '../../api/cloud/wait-for-operation.js';
Expand All @@ -16,20 +15,14 @@ const SYNC_CONFIG_VALIDATION_FLAGS = generateValidationTestFlags({
limitOptions: [ValidationTest['SYNC-CONFIG']]
});

export default class DeploySyncConfig extends BaseDeployCommand {
export default class DeploySyncConfig extends WithSyncConfigFilePath(BaseDeployCommand) {
static description = 'Deploy only sync config changes.';
static examples = [
'<%= config.bin %> <%= command.id %>',
'<%= config.bin %> <%= command.id %> --instance-id=<id> --project-id=<id>'
];
static flags = {
...BaseDeployCommand.flags,
...SYNC_CONFIG_VALIDATION_FLAGS.flags,
'sync-config-file-path': Flags.file({
description:
'Path to a sync config file. If provided, this file will be validated and deployed instead of the default sync-config.yaml.',
exists: true
})
...SYNC_CONFIG_VALIDATION_FLAGS.flags
};
static summary = '[Cloud only] Deploy only local sync config to the linked Cloud instance.';

Expand Down Expand Up @@ -84,9 +77,10 @@ export default class DeploySyncConfig extends BaseDeployCommand {
const { linked } = project;
const deployTimeoutMs = (flags['deploy-timeout'] ?? DEFAULT_DEPLOY_TIMEOUT_MS / 1000) * 1000;

const syncConfigFilePath = flags['sync-config-file-path'];
if (syncConfigFilePath) {
project.syncRulesContent = readFileSync(syncConfigFilePath, 'utf8');
if (!project.syncRulesContent) {
this.styledError({
message: `Sync config content not loaded. Ensure sync config is present and valid.`
});
}

if (!project.syncRulesContent) {
Expand Down
3 changes: 1 addition & 2 deletions cli/src/commands/destroy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ export default class Destroy extends CloudInstanceCommand {
confirm: Flags.string({
description: 'Set to "yes" to confirm destruction of the instance.',
options: ['yes']
}),
...CloudInstanceCommand.flags
})
};
static summary = '[Cloud only] Permanently destroy the linked Cloud instance.';

Expand Down
3 changes: 1 addition & 2 deletions cli/src/commands/fetch/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ export default class FetchConfig extends CloudInstanceCommand {
default: 'yaml',
description: 'Output format: yaml or json.',
options: ['json', 'yaml']
}),
...CloudInstanceCommand.flags
})
};
static summary = '[Cloud only] Print linked Cloud instance config (YAML or JSON).';

Expand Down
3 changes: 1 addition & 2 deletions cli/src/commands/fetch/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ export default class FetchStatus extends SharedInstanceCommand {
default: 'human',
description: 'Output format: human-readable, json, or yaml.',
options: ['human', 'json', 'yaml']
}),
...SharedInstanceCommand.flags
})
};
static summary = 'Show instance diagnostics (connections, sync config, replication).';

Expand Down
10 changes: 5 additions & 5 deletions cli/src/commands/generate/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
createCloudClient,
createSelfHostedClient,
SelfHostedProject,
SharedInstanceCommand
SharedInstanceCommand,
WithSyncConfigFilePath
} from '@powersync/cli-core';
import { routes } from '@powersync/management-types';
import { schemaGenerators, SqlSyncRules, StaticSchema } from '@powersync/service-sync-rules';
Expand All @@ -13,7 +14,7 @@ import { writeFileSync } from 'node:fs';
import { fetchCloudSyncRulesContent } from '../../api/cloud/fetch-cloud-sync-rules-content.js';
import { fetchSelfHostedSyncRulesContent } from '../../api/self-hosted/fetch-self-hosted-sync-rules-content.js';

export default class GenerateSchema extends SharedInstanceCommand {
export default class GenerateSchema extends WithSyncConfigFilePath(SharedInstanceCommand) {
static description =
'Generate a client-side schema file from the instance database schema and sync config. Supports multiple output types (e.g. type, dart). Requires a linked instance. Cloud and self-hosted.';
static examples = [
Expand All @@ -30,14 +31,13 @@ export default class GenerateSchema extends SharedInstanceCommand {
'output-path': Flags.string({
description: 'Path to output the schema file.',
required: true
}),
...SharedInstanceCommand.flags
})
};
static summary = 'Generate client schema file from instance schema and sync config.';

async getCloudSchema(project: CloudProject): Promise<routes.GetSchemaResponse> {
const { linked } = project;
const client = await createCloudClient();
const client = createCloudClient();
return client.getInstanceSchema({
app_id: linked.project_id,
id: linked.instance_id,
Expand Down
3 changes: 1 addition & 2 deletions cli/src/commands/generate/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ export default class GenerateToken extends SharedInstanceCommand {
subject: Flags.string({
description: 'Subject of the token.',
required: true
}),
...SharedInstanceCommand.flags
})
};
static summary = 'Generate a development JWT for client connections.';

Expand Down
3 changes: 0 additions & 3 deletions cli/src/commands/init/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ export default class InitCloud extends InstanceCommand {
'<%= config.bin %> <%= command.id %>',
'<%= config.bin %> <%= command.id %> --directory=powersync'
];
static flags = {
...InstanceCommand.flags
};
static summary = 'Scaffold a PowerSync Cloud config directory from a template.';

async run(): Promise<void> {
Expand Down
3 changes: 0 additions & 3 deletions cli/src/commands/init/self-hosted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ export default class InitSelfHosted extends InstanceCommand {
'<%= config.bin %> <%= command.id %>',
'<%= config.bin %> <%= command.id %> --directory=powersync'
];
static flags = {
...InstanceCommand.flags
};
static summary = 'Scaffold a PowerSync self-hosted config directory from a template.';

async run(): Promise<void> {
Expand Down
4 changes: 1 addition & 3 deletions cli/src/commands/link/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
ensureServiceTypeMatches,
env,
getDefaultOrgId,
InstanceCommand,
ServiceType
} from '@powersync/cli-core';

Expand Down Expand Up @@ -44,8 +43,7 @@ export default class LinkCloud extends CloudInstanceCommand {
default: env.PROJECT_ID,
description: 'Project ID. Resolved: flag → PROJECT_ID → cli.yaml.',
required: true
}),
...InstanceCommand.flags
})
};
static summary = '[Cloud only] Link to a PowerSync Cloud instance (or create one with --create).';

Expand Down
4 changes: 1 addition & 3 deletions cli/src/commands/link/self-hosted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
CommandHelpGroup,
ensureServiceTypeMatches,
env,
InstanceCommand,
parseYamlFile,
SelfHostedInstanceCommand,
ServiceType
Expand All @@ -25,8 +24,7 @@ export default class LinkSelfHosted extends SelfHostedInstanceCommand {
'api-url': Flags.string({
description: 'Self-hosted PowerSync API base URL (e.g. https://powersync.example.com).',
required: true
}),
...InstanceCommand.flags
})
};
static summary = 'Link to a self-hosted PowerSync instance by API URL.';

Expand Down
3 changes: 1 addition & 2 deletions cli/src/commands/migrate/sync-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ export default class MigrateSyncRules extends SharedInstanceCommand {
'output-file': Flags.string({
description: 'Path to the output sync streams file. Defaults to overwrite the input file.',
required: false
}),
...SharedInstanceCommand.flags
})
};
static summary = 'Migrates Sync Rules to Sync Streams';

Expand Down
1 change: 0 additions & 1 deletion cli/src/commands/pull/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export default class PullInstance extends CloudInstanceCommand {
'<%= config.bin %> <%= command.id %> --instance-id=<id> --project-id=<id> --org-id=<org-id>'
];
static flags = {
...CloudInstanceCommand.flags,
overwrite: Flags.boolean({
description:
'Overwrite existing service.yaml and sync-config.yaml if they exist. By default, if these files already exist, the fetched configs will be written to service-fetched.yaml and sync-fetched.yaml to avoid overwriting local changes.'
Expand Down
3 changes: 1 addition & 2 deletions cli/src/commands/stop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ export default class Stop extends CloudInstanceCommand {
confirm: Flags.string({
description: 'Set to "yes" to confirm stopping the instance.',
options: ['yes']
}),
...CloudInstanceCommand.flags
})
};
static summary = '[Cloud only] Stop the linked Cloud instance (restart with deploy).';

Expand Down
13 changes: 9 additions & 4 deletions cli/src/commands/validate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Flags } from '@oclif/core';
import { CloudProject, SelfHostedProject, SharedInstanceCommand, ValidationResult } from '@powersync/cli-core';
import {
CloudProject,
SelfHostedProject,
SharedInstanceCommand,
ValidationResult,
WithSyncConfigFilePath
} from '@powersync/cli-core';

import { parseLocalCloudServiceConfig } from '../api/parse-local-cloud-service-config.js';
import { getCloudValidations } from '../api/validations/cloud-validations.js';
Expand All @@ -9,7 +15,7 @@ import { formatValidationJson, formatValidationYaml } from '../api/validations/v
import { ValidationsRunner } from '../api/validations/ValidationsRunner.js';
import { ValidationTest } from '../api/validations/ValidationTestDefinition.js';

export default class Validate extends SharedInstanceCommand {
export default class Validate extends WithSyncConfigFilePath(SharedInstanceCommand) {
static description =
'Run validation checks on local config: config schema, database connections, and sync config. Requires a linked instance. Works with Cloud and self-hosted.';
static examples = [
Expand All @@ -23,8 +29,7 @@ export default class Validate extends SharedInstanceCommand {
description: 'Output format: human-readable, json, or yaml.',
options: ['human', 'json', 'yaml']
}),
...GENERAL_VALIDATION_FLAG_HELPERS.flags,
...SharedInstanceCommand.flags
...GENERAL_VALIDATION_FLAG_HELPERS.flags
};
static summary = 'Validate config schema, connections, and sync config before deploy.';

Expand Down
Loading
Loading