11import { ux } from '@oclif/core' ;
22import { CLICloudConfig } from '@powersync/cli-schemas' ;
3- import { existsSync , mkdirSync , writeFileSync } from 'node:fs' ;
4- import { join } from 'node:path' ;
3+ import { existsSync , mkdirSync , readFileSync , writeFileSync } from 'node:fs' ;
4+ import { dirname , join } from 'node:path' ;
5+ import { fileURLToPath } from 'node:url' ;
56import * as t from 'ts-codec' ;
6- import { Document } from 'yaml' ;
7+ import { Document , isMap , type Node , type Pair , parseDocument , YAMLMap } from 'yaml' ;
78
89import {
910 CloudInstanceCommand ,
@@ -19,65 +20,152 @@ import { writeCloudLink } from '../../api/cloud/write-cloud-link.js';
1920const SERVICE_FETCHED_FILENAME = 'service-fetched.yaml' ;
2021const SYNC_FETCHED_FILENAME = 'sync-fetched.yaml' ;
2122
23+ const PULL_CONFIG_HEADER = `# PowerSync Cloud config (fetched from cloud)
24+ # yaml-language-server: $schema=https://unpkg.com/@powersync/cli-schemas@latest/json-schema/cli-config.json
25+ #
26+ ` ;
27+
28+ const __dirname = dirname ( fileURLToPath ( import . meta. url ) ) ;
29+ const CLOUD_SERVICE_TEMPLATE_PATH = join (
30+ __dirname ,
31+ '..' ,
32+ '..' ,
33+ '..' ,
34+ 'templates' ,
35+ 'cloud' ,
36+ 'powersync' ,
37+ 'service.yaml'
38+ ) ;
39+
2240type JSONSchemaObject = {
2341 description ?: string ;
2442 properties ?: Record < string , JSONSchemaObject > ;
2543 items ?: JSONSchemaObject ;
2644} ;
2745
28- function getDescriptionFromSchema ( schema : JSONSchemaObject | undefined , path : string [ ] ) : string | undefined {
29- if ( ! schema || path . length === 0 ) return schema ?. description ;
30- const [ head , ...rest ] = path ;
31- const next = schema . properties ?. [ head ] ;
32- return rest . length === 0 ? next ?. description : getDescriptionFromSchema ( next , rest ) ;
46+ function pairKey ( pair : Pair < unknown , unknown > ) : string | undefined {
47+ const k = pair . key as { value ?: string } | undefined ;
48+ return k ?. value ;
3349}
3450
35- function setCommentsOnMap (
36- map : { items : Array < { key : unknown ; value : unknown } > } ,
37- schema : JSONSchemaObject | undefined ,
38- path : string [ ]
39- ) : void {
40- if ( ! schema ?. properties ) return ;
41- for ( const pair of map . items ) {
42- const keyStr =
43- typeof pair . key === 'object' && pair . key !== null && 'value' in pair . key
44- ? String ( ( pair . key as { value : unknown } ) . value )
45- : String ( pair . key ) ;
46- const keyPath = [ ...path , keyStr ] ;
47- const desc = getDescriptionFromSchema ( schema , keyPath ) ;
48- const value = pair . value as { commentBefore ?: string | null ; items ?: unknown [ ] } ;
49- if ( value && typeof value === 'object' && desc ) {
50- value . commentBefore = desc ;
51- }
52- if ( value && typeof value === 'object' && Array . isArray ( ( value as { items ?: unknown [ ] } ) . items ) ) {
53- const subSchema = schema . properties ?. [ keyStr ] ;
54- const innerMap = value as { items : Array < { key : unknown ; value : unknown } > } ;
55- setCommentsOnMap ( innerMap , subSchema , keyPath ) ;
56- }
51+ /** Find the Pair node for a top-level key in a parsed YAML map. */
52+ function findMapPair ( contents : unknown , key : string ) : Pair < unknown , unknown > | null {
53+ if ( ! contents || typeof ( contents as { items ?: unknown [ ] } ) . items !== 'object' ) return null ;
54+ const items = ( contents as { items : Pair < unknown , unknown > [ ] } ) . items ;
55+ const pair = items . find ( ( p ) => pairKey ( p ) === key ) ;
56+ return pair ?? null ;
57+ }
58+
59+ /** Index of the pair with the given key in the map's items, or -1. */
60+ function findMapPairIndex ( map : YAMLMap , key : string ) : number {
61+ const items = map . items ?? [ ] ;
62+ return items . findIndex ( ( p ) => pairKey ( p ) === key ) ;
63+ }
64+
65+ /** Insert a pair into the map at the given index (or append if index >= items.length). */
66+ function insertPair ( map : YAMLMap , index : number , pair : Pair < unknown , unknown > ) : void {
67+ const items = map . items ?? [ ] ;
68+ if ( index < 0 || index >= items . length ) {
69+ items . push ( pair ) ;
70+ } else {
71+ items . splice ( index , 0 , pair ) ;
5772 }
5873}
5974
60- const PULL_CONFIG_HEADER = `# PowerSync Cloud config (fetched from cloud)
61- # yaml-language-server: $schema=https://unpkg.com/@powersync/cli-schemas@latest/json-schema/cli- config.json
62- #
63- ` ;
75+ function hasSection ( config : t . Decoded < typeof CLICloudConfig > , key : 'replication' | 'client_auth' ) : boolean {
76+ const c = config as Record < string , unknown > ;
77+ return c [ key ] !== undefined && c [ key ] !== null ;
78+ }
6479
65- function getSchema ( ) : JSONSchemaObject {
80+ function formatServiceYamlWithComments ( config : t . Decoded < typeof CLICloudConfig > ) : string {
81+ let schema : JSONSchemaObject ;
6682 try {
67- return ( t . generateJSONSchema ( CLICloudConfig ) as JSONSchemaObject ) ?? { } ;
83+ schema = ( t . generateJSONSchema ( CLICloudConfig ) as JSONSchemaObject ) ?? { } ;
6884 } catch {
69- return { } ;
85+ schema = { } ;
86+ }
87+
88+ function getDescriptionFromSchema ( s : JSONSchemaObject | undefined , path : string [ ] ) : string | undefined {
89+ if ( ! s || path . length === 0 ) return s ?. description ;
90+ const [ head , ...rest ] = path ;
91+ const next = s . properties ?. [ head ] ;
92+ return rest . length === 0 ? next ?. description : getDescriptionFromSchema ( next , rest ) ;
93+ }
94+
95+ function setCommentsOnMap (
96+ map : { items : Array < { key : unknown ; value : unknown } > } ,
97+ s : JSONSchemaObject | undefined ,
98+ path : string [ ]
99+ ) : void {
100+ if ( ! s ?. properties ) return ;
101+ for ( const pair of map . items ) {
102+ const keyStr =
103+ typeof pair . key === 'object' && pair . key !== null && 'value' in pair . key
104+ ? String ( ( pair . key as { value : unknown } ) . value )
105+ : String ( pair . key ) ;
106+ const keyPath = [ ...path , keyStr ] ;
107+ const desc = getDescriptionFromSchema ( s , keyPath ) ;
108+ const value = pair . value as { commentBefore ?: string | null ; items ?: unknown [ ] } ;
109+ if ( value && typeof value === 'object' && desc ) {
110+ value . commentBefore = desc ;
111+ }
112+ if ( value && typeof value === 'object' && Array . isArray ( ( value as { items ?: unknown [ ] } ) . items ) ) {
113+ const subSchema = s . properties ?. [ keyStr ] ;
114+ const innerMap = value as { items : Array < { key : unknown ; value : unknown } > } ;
115+ setCommentsOnMap ( innerMap , subSchema , keyPath ) ;
116+ }
117+ }
70118 }
71- }
72119
73- function formatServiceYamlWithComments ( config : t . Decoded < typeof CLICloudConfig > ) : string {
74- const schema = getSchema ( ) ;
75120 const doc = new Document ( config ) ;
76121 const contents = doc . contents as { items ?: Array < { key : unknown ; value : unknown } > } | null ;
77122 if ( contents && typeof contents === 'object' && Array . isArray ( contents . items ) && contents . items . length > 0 ) {
78123 setCommentsOnMap ( { items : contents . items } , Object . keys ( schema ) . length > 0 ? schema : undefined , [ ] ) ;
79124 }
80- return PULL_CONFIG_HEADER + doc . toString ( ) ;
125+
126+ let templateDoc : ReturnType < typeof parseDocument > | null = null ;
127+ try {
128+ templateDoc = parseDocument ( readFileSync ( CLOUD_SERVICE_TEMPLATE_PATH , 'utf8' ) ) ;
129+ } catch {
130+ // template missing or parse failed
131+ }
132+
133+ if ( templateDoc ?. contents && isMap ( doc . contents ) ) {
134+ const outMap = doc . contents as YAMLMap ;
135+ const templateMap = templateDoc . contents as YAMLMap ;
136+ const replicationPair = findMapPair ( templateMap , 'replication' ) ;
137+ const clientAuthPair = findMapPair ( templateMap , 'client_auth' ) ;
138+
139+ const rep = config . replication as { connections ?: unknown [ ] } | undefined ;
140+ const useTemplateReplication = ! rep || ! Array . isArray ( rep . connections ) || rep . connections . length === 0 ;
141+ if ( useTemplateReplication && replicationPair ) {
142+ const repIdx = findMapPairIndex ( outMap , 'replication' ) ;
143+ if ( repIdx >= 0 ) {
144+ outMap . items [ repIdx ] = replicationPair ;
145+ } else {
146+ const regionIdx = findMapPairIndex ( outMap , 'region' ) ;
147+ insertPair ( outMap , regionIdx >= 0 ? regionIdx + 1 : 0 , replicationPair ) ;
148+ }
149+ }
150+
151+ if ( ! hasSection ( config , 'client_auth' ) && clientAuthPair ) {
152+ insertPair ( outMap , outMap . items ?. length ?? 0 , clientAuthPair ) ;
153+ }
154+ }
155+
156+ let out = PULL_CONFIG_HEADER + doc . toString ( ) ;
157+
158+ if ( hasSection ( config , 'client_auth' ) && templateDoc ?. contents ) {
159+ const templateMap = templateDoc . contents as YAMLMap ;
160+ const clientAuthPair = findMapPair ( templateMap , 'client_auth' ) ;
161+ if ( clientAuthPair ?. value != null ) {
162+ const commentDoc = new Document ( ) ;
163+ commentDoc . contents = clientAuthPair . value as Node ;
164+ out = out . replace ( / \n ? $ / , '\n' ) + commentDoc . toString ( ) . replace ( / \n $ / , '' ) ;
165+ }
166+ }
167+
168+ return out ;
81169}
82170
83171export default class PullConfig extends CloudInstanceCommand {
0 commit comments