diff --git a/cli/README.md b/cli/README.md index e04f469..59d3281 100644 --- a/cli/README.md +++ b/cli/README.md @@ -256,6 +256,7 @@ See [docs/usage.md](../docs/usage.md) for full usage and resolution order (flags - [`powersync autocomplete [SHELL]`](#powersync-autocomplete-shell) - [`powersync commands`](#powersync-commands) +- [`powersync configure ide`](#powersync-configure-ide) - [`powersync deploy`](#powersync-deploy) - [`powersync deploy service-config`](#powersync-deploy-service-config) - [`powersync deploy sync-config`](#powersync-deploy-sync-config) @@ -353,6 +354,26 @@ DESCRIPTION _See code: [@oclif/plugin-commands](https://github.com/oclif/plugin-commands/blob/v4.1.40/src/commands/commands.ts)_ +## `powersync configure ide` + +Configure your IDE for the best PowerSync CLI developer experience. + +``` +USAGE + $ powersync configure ide + +DESCRIPTION + Configure your IDE for the best PowerSync CLI developer experience. + + Configure or guide your IDE setup for the best PowerSync CLI developer experience. Enables YAML schema validation and + autocompletion, sets up !env custom tag support, and patches existing config files with language server directives. + +EXAMPLES + $ powersync configure ide +``` + +_See code: [src/commands/configure/ide.ts](https://github.com/powersync-ja/powersync-js/blob/v0.0.0/src/commands/configure/ide.ts)_ + ## `powersync deploy` [Cloud only] Deploy local config to the linked Cloud instance (connections + auth + sync config). @@ -901,10 +922,7 @@ Scaffold a PowerSync Cloud config directory from a template. ``` USAGE - $ powersync init cloud [--directory ] [--vscode] - -FLAGS - --vscode Configure the workspace with .vscode settings for YAML custom tags (!env). + $ powersync init cloud [--directory ] PROJECT FLAGS --directory= [default: powersync] Directory containing PowerSync config. Defaults to "powersync". This is @@ -919,7 +937,7 @@ DESCRIPTION EXAMPLES $ powersync init cloud - $ powersync init cloud --directory=powersync --vscode + $ powersync init cloud --directory=powersync ``` _See code: [src/commands/init/cloud.ts](https://github.com/powersync-ja/powersync-js/blob/v0.0.0/src/commands/init/cloud.ts)_ @@ -930,10 +948,7 @@ Scaffold a PowerSync self-hosted config directory from a template. ``` USAGE - $ powersync init self-hosted [--directory ] [--vscode] - -FLAGS - --vscode Configure the workspace with .vscode settings for YAML custom tags (!env). + $ powersync init self-hosted [--directory ] PROJECT FLAGS --directory= [default: powersync] Directory containing PowerSync config. Defaults to "powersync". This is @@ -949,7 +964,7 @@ DESCRIPTION EXAMPLES $ powersync init self-hosted - $ powersync init self-hosted --directory=powersync --vscode + $ powersync init self-hosted --directory=powersync ``` _See code: [src/commands/init/self-hosted.ts](https://github.com/powersync-ja/powersync-js/blob/v0.0.0/src/commands/init/self-hosted.ts)_ diff --git a/cli/src/api/ide/configure-vscode-ide.ts b/cli/src/api/ide/configure-vscode-ide.ts new file mode 100644 index 0000000..33d4c92 --- /dev/null +++ b/cli/src/api/ide/configure-vscode-ide.ts @@ -0,0 +1,104 @@ +import { ux } from '@oclif/core'; +import { + CLI_FILENAME, + SERVICE_FILENAME, + SYNC_FILENAME, + YAML_CLI_SCHEMA, + YAML_SERVICE_SCHEMA, + YAML_SYNC_RULES_SCHEMA +} from '@powersync/cli-core'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const VSCODE_YAML_TAGS = ['!env scalar']; + +/** Maps each known config filename to its yaml-language-server schema comment. */ +const YAML_SCHEMA_COMMENTS: Record = { + [CLI_FILENAME]: YAML_CLI_SCHEMA, + [SERVICE_FILENAME]: YAML_SERVICE_SCHEMA, + [SYNC_FILENAME]: YAML_SYNC_RULES_SCHEMA +}; + +/** + * A pluggable function signature for configuring a specific IDE. + * Receives the workspace root (for IDE settings), the list of discovered project + * directories to scan, and a log callback for producing output. + */ +export type IdeConfigurator = (workspaceRoot: string, projectDirs: string[], log: (message: string) => void) => void; + +/** + * Configures the VSCode workspace for PowerSync YAML editing and prints guidance: + * - Writes/merges .vscode/settings.json with yaml.customTags so the !env tag is recognised. + * - Scans each projectDir for known config files and prepends a yaml-language-server schema + * comment to any file that does not already have one. + * - Prints a summary of changes, extension recommendation, and schema comment reference. + */ +export function configureVscodeIde(workspaceRoot: string, projectDirs: string[], log: (message: string) => void): void { + const vscodeDir = join(workspaceRoot, '.vscode'); + const settingsPath = join(vscodeDir, 'settings.json'); + + let settings: Record = {}; + if (existsSync(settingsPath)) { + try { + const raw = readFileSync(settingsPath, 'utf8'); + settings = JSON.parse(raw) as Record; + } catch { + // If invalid JSON, overwrite with our settings. + } + } + + const currentTags = (settings['yaml.customTags'] ?? []) as string[]; + settings['yaml.customTags'] = [...new Set([...currentTags, ...VSCODE_YAML_TAGS])]; + mkdirSync(vscodeDir, { recursive: true }); + writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8'); + + const filesUpdated: string[] = []; + + for (const projectDir of projectDirs) { + for (const [filename, schemaComment] of Object.entries(YAML_SCHEMA_COMMENTS)) { + const filePath = join(projectDir, filename); + if (!existsSync(filePath)) continue; + + const content = readFileSync(filePath, 'utf8'); + if (!content.includes('yaml-language-server:')) { + writeFileSync(filePath, `${schemaComment}\n\n${content}`); + filesUpdated.push(filePath); + } + } + } + + const lines: string[] = [ + ux.colorize('green', 'VSCode configured for PowerSync YAML editing!'), + '', + `✔ Updated .vscode/settings.json with yaml.customTags: ${JSON.stringify(VSCODE_YAML_TAGS)}` + ]; + + if (filesUpdated.length > 0) { + lines.push('', 'Added yaml-language-server schema comments to:'); + for (const f of filesUpdated) { + lines.push(` ✔ ${f}`); + } + } + + lines.push( + '', + ux.colorize('cyan', 'Recommended: Install the YAML extension'), + 'Install the Red Hat YAML extension for VSCode to get schema validation and autocompletion:', + ux.colorize('blue', ' https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml'), + '', + ux.colorize('cyan', 'Language server schema comments'), + 'The following comments at the top of each YAML config file activate schema support.', + 'They are added automatically when you run powersync init, but you can also add them manually:', + '', + ux.colorize('dim', '# service.yaml'), + ux.colorize('gray', YAML_SERVICE_SCHEMA), + '', + ux.colorize('dim', '# sync-config.yaml'), + ux.colorize('gray', YAML_SYNC_RULES_SCHEMA), + '', + ux.colorize('dim', '# cli.yaml'), + ux.colorize('gray', YAML_CLI_SCHEMA) + ); + + log(lines.join('\n')); +} diff --git a/cli/src/api/write-vscode-settings-for-yaml-env.ts b/cli/src/api/write-vscode-settings-for-yaml-env.ts deleted file mode 100644 index bdad7e3..0000000 --- a/cli/src/api/write-vscode-settings-for-yaml-env.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; -import { join } from 'node:path'; - -const VSCODE_YAML_TAGS = ['!env scalar']; - -/** - * Writes or merges .vscode/settings.json in the workspace root so that YAML files - * get proper schema support for the !env custom tag. - */ -export function writeVscodeSettingsForYamlEnv(workspaceRoot: string): void { - const vscodeDir = join(workspaceRoot, '.vscode'); - const settingsPath = join(vscodeDir, 'settings.json'); - - let settings: Record = {}; - if (existsSync(settingsPath)) { - try { - const raw = readFileSync(settingsPath, 'utf8'); - settings = JSON.parse(raw) as Record; - } catch { - // If invalid JSON, overwrite with our settings - } - } - - const currentSettings = (settings['yaml.customTags'] ?? []) as string[]; - const mergedTags = [...new Set([...currentSettings, ...VSCODE_YAML_TAGS])]; - settings['yaml.customTags'] = mergedTags; - mkdirSync(vscodeDir, { recursive: true }); - writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8'); -} diff --git a/cli/src/commands/configure/ide.ts b/cli/src/commands/configure/ide.ts new file mode 100644 index 0000000..f89b4bb --- /dev/null +++ b/cli/src/commands/configure/ide.ts @@ -0,0 +1,68 @@ +import { select } from '@inquirer/prompts'; +import { ux } from '@oclif/core'; +import { CLI_FILENAME, parseYamlFile, PowerSyncCommand } from '@powersync/cli-core'; +import { CLIConfig } from '@powersync/cli-schemas'; +import { readdirSync, statSync } from 'node:fs'; +import { join } from 'node:path'; + +import { configureVscodeIde, type IdeConfigurator } from '../../api/ide/configure-vscode-ide.js'; + +const IDE_CONFIGURATORS: Record = { + vscode: configureVscodeIde +}; + +/** + * Scans the current working directory for subdirectories that contain a valid + * PowerSync cli.yaml. Returns their absolute paths. + */ +function findLinkedProjectDirs(cwd: string): string[] { + const projectDirs: string[] = []; + + for (const entry of readdirSync(cwd)) { + const entryPath = join(cwd, entry); + try { + if (!statSync(entryPath).isDirectory()) continue; + } catch { + continue; + } + + try { + const doc = parseYamlFile(join(entryPath, CLI_FILENAME)); + CLIConfig.decode(doc.contents?.toJSON()); + projectDirs.push(entryPath); + } catch { + // Not a valid PowerSync project — skip. + } + } + + return projectDirs; +} + +export default class ConfigureIde extends PowerSyncCommand { + static description = + 'Configure or guide your IDE setup for the best PowerSync CLI developer experience. Enables YAML schema validation and autocompletion, sets up !env custom tag support, and patches existing config files with language server directives.'; + static examples = ['<%= config.bin %> <%= command.id %>']; + static summary = 'Configure your IDE for the best PowerSync CLI developer experience.'; + + async run(): Promise { + await this.parse(ConfigureIde); + + this.log( + `Tip: use ${ux.colorize('blue', 'powersync edit config')} for a complete in-browser editing experience.\n` + ); + + const ide = await select({ + choices: [ + { name: 'VSCode', value: 'vscode' }, + { name: 'Exit', value: 'exit' } + ], + message: 'Select your IDE to configure (only VSCode is supported for now):' + }); + + if (ide === 'exit') return; + + const projectDirs = findLinkedProjectDirs(process.cwd()); + const configurator = IDE_CONFIGURATORS[ide]; + configurator(process.cwd(), projectDirs, (msg) => this.log(msg)); + } +} diff --git a/cli/src/commands/configure/index.ts b/cli/src/commands/configure/index.ts new file mode 100644 index 0000000..8deaa2e --- /dev/null +++ b/cli/src/commands/configure/index.ts @@ -0,0 +1,13 @@ +import { Command } from '@oclif/core'; + +export default class Configure extends Command { + static description = 'Configure your workspace or IDE for PowerSync development.'; + static examples = ['<%= config.bin %> <%= command.id %>']; + static hidden = true; + static summary = 'Configure your workspace or IDE for PowerSync development.'; + + async run(): Promise { + await this.parse(Configure); + this.log('Use a subcommand: configure ide'); + } +} diff --git a/cli/src/commands/init/cloud.ts b/cli/src/commands/init/cloud.ts index 43f3ebe..1da6b97 100644 --- a/cli/src/commands/init/cloud.ts +++ b/cli/src/commands/init/cloud.ts @@ -1,4 +1,4 @@ -import { Flags, ux } from '@oclif/core'; +import { ux } from '@oclif/core'; import { CLI_FILENAME, InstanceCommand, @@ -12,8 +12,6 @@ import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { writeVscodeSettingsForYamlEnv } from '../../api/write-vscode-settings-for-yaml-env.js'; - const __dirname = dirname(fileURLToPath(import.meta.url)); const TEMPLATES_DIR = join(__dirname, '..', '..', '..', 'templates'); @@ -22,20 +20,16 @@ export default class InitCloud extends InstanceCommand { 'Copy a Cloud template into a config directory (default powersync/). Edit service.yaml then run link cloud and deploy.'; static examples = [ '<%= config.bin %> <%= command.id %>', - '<%= config.bin %> <%= command.id %> --directory=powersync --vscode' + '<%= config.bin %> <%= command.id %> --directory=powersync' ]; static flags = { - ...InstanceCommand.flags, - vscode: Flags.boolean({ - default: false, - description: 'Configure the workspace with .vscode settings for YAML custom tags (!env).' - }) + ...InstanceCommand.flags }; static summary = 'Scaffold a PowerSync Cloud config directory from a template.'; async run(): Promise { const { flags } = await this.parse(InitCloud); - const { directory, vscode } = flags; + const { directory } = flags; const targetDir = this.resolveProjectDir(flags); if (existsSync(targetDir)) { @@ -62,10 +56,6 @@ export default class InitCloud extends InstanceCommand { writeFileSync(syncPath, `${YAML_SYNC_RULES_SCHEMA}\n\n${readFileSync(syncPath, 'utf8')}`); writeFileSync(cliPath, `${YAML_CLI_SCHEMA}\n\n${readFileSync(cliPath, 'utf8')}`); - if (vscode) { - writeVscodeSettingsForYamlEnv(process.cwd()); - } - const instructions = [ 'Create a new instance with ', ux.colorize('blue', '\tpowersync link cloud --create --org-id= --project-id='), @@ -86,13 +76,10 @@ export default class InitCloud extends InstanceCommand { 'Configuration files are located in:', `\t${targetDir}`, `Check the ${SERVICE_FILENAME} and ${SYNC_FILENAME} file(s) and configure them by uncommenting the options you would like to use.`, + `Tip: Run ${ux.colorize('blue', 'powersync configure ide')} to configure your IDE for YAML schema support.`, '', instructions ]; - if (vscode) { - lines.splice(6, 0, 'Added .vscode/settings.json for YAML !env tag support.'); - lines.splice(7, 0, ''); - } this.log(lines.join('\n')); } diff --git a/cli/src/commands/init/self-hosted.ts b/cli/src/commands/init/self-hosted.ts index fdc8beb..8d88e5d 100644 --- a/cli/src/commands/init/self-hosted.ts +++ b/cli/src/commands/init/self-hosted.ts @@ -1,4 +1,4 @@ -import { Flags, ux } from '@oclif/core'; +import { ux } from '@oclif/core'; import { CLI_FILENAME, InstanceCommand, @@ -12,8 +12,6 @@ import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { writeVscodeSettingsForYamlEnv } from '../../api/write-vscode-settings-for-yaml-env.js'; - const __dirname = dirname(fileURLToPath(import.meta.url)); const TEMPLATES_DIR = join(__dirname, '..', '..', '..', 'templates'); @@ -22,20 +20,16 @@ export default class InitSelfHosted extends InstanceCommand { 'Copy a self-hosted template into a config directory (default powersync/). Configure service.yaml with your self-hosted instance details.'; static examples = [ '<%= config.bin %> <%= command.id %>', - '<%= config.bin %> <%= command.id %> --directory=powersync --vscode' + '<%= config.bin %> <%= command.id %> --directory=powersync' ]; static flags = { - ...InstanceCommand.flags, - vscode: Flags.boolean({ - default: false, - description: 'Configure the workspace with .vscode settings for YAML custom tags (!env).' - }) + ...InstanceCommand.flags }; static summary = 'Scaffold a PowerSync self-hosted config directory from a template.'; async run(): Promise { const { flags } = await this.parse(InitSelfHosted); - const { directory, vscode } = flags; + const { directory } = flags; const targetDir = this.resolveProjectDir(flags); if (existsSync(targetDir)) { @@ -62,10 +56,6 @@ export default class InitSelfHosted extends InstanceCommand { writeFileSync(syncPath, `${YAML_SYNC_RULES_SCHEMA}\n\n${readFileSync(syncPath, 'utf8')}`); writeFileSync(cliPath, `${YAML_CLI_SCHEMA}\n\n${readFileSync(cliPath, 'utf8')}`); - if (vscode) { - writeVscodeSettingsForYamlEnv(process.cwd()); - } - const instructions = [ 'Self Hosted projects currently require external configuration for starting and deploying.', `Configure the ${SERVICE_FILENAME} file with your self-hosted instance details.`, @@ -79,13 +69,10 @@ export default class InitSelfHosted extends InstanceCommand { 'Configuration files are located in:', `\t${targetDir}`, `Check the ${SERVICE_FILENAME} and ${SYNC_FILENAME} file(s) and configure them by uncommenting the options you would like to use.`, + `Tip: Run ${ux.colorize('blue', 'powersync configure ide')} to configure your IDE for YAML schema support.`, '', instructions ]; - if (vscode) { - lines.splice(6, 0, 'Added .vscode/settings.json for YAML !env tag support.'); - lines.splice(7, 0, ''); - } this.log(lines.join('\n')); }