Skip to content

Commit 59edbca

Browse files
accounts client and list instances command
1 parent 34e0ade commit 59edbca

File tree

8 files changed

+377
-31
lines changed

8 files changed

+377
-31
lines changed

cli/package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,19 @@
88
},
99
"bugs": "https://github.com/powersync-ja/cli/issues",
1010
"dependencies": {
11-
"@powersync/cli-core": "workspace:*",
12-
"@powersync/cli-plugin-docker": "workspace:*",
1311
"@inquirer/prompts": "^7.2.0",
1412
"@oclif/core": "^4",
1513
"@oclif/plugin-help": "^6",
1614
"@oclif/plugin-plugins": "^5",
15+
"@powersync/cli-core": "workspace:*",
16+
"@powersync/cli-plugin-docker": "workspace:*",
1717
"@powersync/cli-schemas": "workspace:*",
18-
"@powersync/service-sync-rules": "^0.30.0",
1918
"@powersync/management-client": "0.0.1",
20-
"@powersync/service-types": "^0.13.3",
2119
"@powersync/management-types": "0.0.1",
20+
"@powersync/service-sync-rules": "^0.30.0",
21+
"@powersync/service-types": "^0.13.3",
2222
"jose": "^6.1.3",
23+
"lodash": "^4.17.23",
2324
"ora": "^9.0.0",
2425
"ts-codec": "^1.3.0",
2526
"yaml": "^2"
@@ -28,6 +29,7 @@
2829
"@eslint/compat": "^1",
2930
"@oclif/prettier-config": "^0.2.1",
3031
"@oclif/test": "^4",
32+
"@types/lodash": "^4.17.23",
3133
"@types/node": "^24",
3234
"eslint": "^9",
3335
"eslint-config-oclif": "^6",
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { Command, Flags, ux } from '@oclif/core';
2+
import { createAccountsHubClient, createCloudClient } from '@powersync/cli-core';
3+
import sortBy from 'lodash/sortBy.js';
4+
import ora from 'ora';
5+
6+
type Instance = {
7+
id: string;
8+
name: string;
9+
has_config: boolean;
10+
deployable: boolean;
11+
};
12+
13+
export type Project = {
14+
id: string;
15+
name: string;
16+
instances: Instance[];
17+
};
18+
19+
type ProjectMap = {
20+
[project_id: string]: Project;
21+
};
22+
23+
type Organization = {
24+
id: string;
25+
name: string;
26+
projects: ProjectMap;
27+
};
28+
29+
type OrganizationMap = {
30+
[org_id: string]: Organization;
31+
};
32+
33+
export default class FetchInstances extends Command {
34+
static description =
35+
'List PowerSync Cloud instances in the current org and project. Use with a linked directory or pass --org-id and --project-id. Cloud only.';
36+
static summary = 'List Cloud instances in the current org/project.';
37+
38+
static flags = {
39+
'org-id': Flags.string({
40+
description: 'Optional Organization ID. Defaults to all organizations.',
41+
required: false
42+
}),
43+
'project-id': Flags.string({
44+
description: 'Optional Project ID. Defaults to all projects in the org.',
45+
required: false
46+
}),
47+
output: Flags.string({
48+
description: 'Output format: human or json.',
49+
options: ['human', 'json'],
50+
default: 'human'
51+
})
52+
};
53+
54+
async run(): Promise<void> {
55+
const accountsClient = await createAccountsHubClient();
56+
const managementClient = await createCloudClient();
57+
58+
const { flags } = await this.parse(FetchInstances);
59+
const { org_id, project_id } = flags;
60+
61+
const instanceMap: OrganizationMap = {};
62+
let totalOrgs: number | undefined;
63+
let processedOrgs = 0;
64+
let spinnerStarted = false;
65+
66+
const spinner = ora({
67+
text: 'Fetching instances...',
68+
stream: process.stdout
69+
});
70+
71+
for await (const page of accountsClient.listOrganizations.paginate({ id: org_id })) {
72+
const { objects: organizations, total } = page;
73+
if (totalOrgs === undefined) {
74+
totalOrgs = total;
75+
if (total > 0) {
76+
spinner.start();
77+
spinnerStarted = true;
78+
}
79+
}
80+
81+
for (const organization of organizations) {
82+
spinner.text = `Fetching org ${processedOrgs + 1} of ${totalOrgs}...`;
83+
const orgMap = (instanceMap[organization.id] = {
84+
id: organization.id,
85+
name: organization.label,
86+
projects: {} as ProjectMap
87+
});
88+
89+
let totalProjects: number | undefined;
90+
let processedProjects = 0;
91+
92+
for await (const projectPage of accountsClient.listProjects.paginate({
93+
id: project_id,
94+
org_id: organization.id
95+
})) {
96+
const { objects: projects, total } = projectPage;
97+
if (totalProjects === undefined) {
98+
totalProjects = total;
99+
}
100+
101+
for (const project of projects) {
102+
spinner.text = `Fetching org ${processedOrgs + 1} of ${totalOrgs}, project ${processedProjects + 1} of ${totalProjects}...`;
103+
const projectMap = (orgMap.projects[project.id] = {
104+
id: project.id,
105+
name: project.name,
106+
instances: [] as Instance[]
107+
});
108+
const instances = await managementClient.listInstances({
109+
app_id: project.id,
110+
org_id: organization.id
111+
});
112+
projectMap.instances.push(...instances.instances);
113+
processedProjects++;
114+
}
115+
}
116+
processedOrgs++;
117+
}
118+
}
119+
120+
if (spinnerStarted) {
121+
spinner.stop();
122+
}
123+
124+
if (flags.output === 'human') {
125+
// Log in human readable format
126+
for (const org of sortBy(Object.values(instanceMap), 'name')) {
127+
this.log(`${ux.colorize('blue', 'Organization: ')} ${org.name} ${ux.colorize('gray', `id=${org.id}`)}`);
128+
for (const project of sortBy(Object.values(org.projects), 'name')) {
129+
this.log(`\t${ux.colorize('blue', 'Project: ')} ${project.name} ${ux.colorize('gray', `id=${project.id}`)}`);
130+
for (const instance of sortBy(project.instances, 'name')) {
131+
this.log(
132+
`\t\t${ux.colorize('blue', 'Instance: ')} ${instance.name} ${ux.colorize('gray', `id=${instance.id}`)} ${ux.colorize('gray', `has_config=${instance.has_config}`)} ${ux.colorize('gray', `deployable=${instance.deployable}`)}`
133+
);
134+
}
135+
}
136+
this.log('');
137+
}
138+
} else if (flags.output === 'json') {
139+
this.log(ux.colorize('gray', JSON.stringify(instanceMap, null, '\t')));
140+
}
141+
}
142+
}

cli/src/commands/fetch/instances/index.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

examples/self-hosted/local-postgres-node/powersync/docker/docker-compose.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ services:
2121
volumes:
2222
- ../service.yaml:/config/service.yaml
2323
- ../sync.yaml:/config/sync.yaml
24+
2425
environment:
2526
POWERSYNC_CONFIG_PATH: /config/service.yaml
2627
NODE_OPTIONS: --max-old-space-size=1000
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* TODO, replace this with a new public accounts SDK in future
3+
*/
4+
5+
/**
6+
* @fileoverview API client for AccountsHub service
7+
* @module lib/api/clients/AccountsHubClient
8+
*/
9+
10+
import * as sdk from '@journeyapps-labs/common-sdk';
11+
import { ux } from '@oclif/core';
12+
import { getSecureStorage } from '../services/SecureStorage.js';
13+
import { env } from '../utils/env.js';
14+
15+
/**
16+
* Client for interacting with the AccountsHub API service.
17+
*
18+
* Handles:
19+
* - User authentication and profile management
20+
* - Organization management
21+
* - Project listing
22+
* - User management within organizations
23+
*/
24+
25+
export type Org = {
26+
id: string;
27+
label: string;
28+
};
29+
30+
export type Project = {
31+
id: string;
32+
name: string;
33+
};
34+
35+
export class AccountsHubClientSDKClient<C extends sdk.NetworkClient = sdk.NetworkClient> extends sdk.SDKClient<C> {
36+
getOrganization = this.createEndpoint<{ id: string }, Org>({
37+
path: '/api/accounts/v5/organizations/get',
38+
method: 'post'
39+
});
40+
41+
listOrganizations = sdk.createPaginatedEndpoint(
42+
this.createEndpoint<sdk.PaginationParams & { id?: string }, sdk.PaginationResponse & { objects: Org[] }>({
43+
path: '/api/accounts/v5/organizations/list',
44+
method: 'post'
45+
})
46+
);
47+
48+
listProjects = sdk.createPaginatedEndpoint(
49+
this.createEndpoint<
50+
sdk.PaginationParams & { org_id?: string; id?: string },
51+
sdk.PaginationResponse & { objects: Project[] }
52+
>({
53+
path: '/api/accounts/v5/apps/list',
54+
method: 'post'
55+
})
56+
);
57+
}
58+
59+
/**
60+
* Creates a PowerSync Accounts Hub Client for the Cloud.
61+
* Uses the token stored by the login command (secure storage, e.g. macOS Keychain).
62+
*/
63+
export async function createAccountsHubClient(): Promise<AccountsHubClientSDKClient> {
64+
const storage = getSecureStorage();
65+
const token = env.TOKEN || (await storage.getToken());
66+
if (!token) {
67+
throw new Error(
68+
`Not logged in. Run ${ux.colorize('blue', 'powersync login')} to authenticate (you will be prompted for your token). Login is supported on macOS (other platforms coming soon).`
69+
);
70+
}
71+
return new AccountsHubClientSDKClient({
72+
client: sdk.createWebNetworkClient({
73+
headers: () => ({
74+
Authorization: `Bearer ${token}`
75+
})
76+
}),
77+
endpoint: env._PS_ACCOUNTS_HUB_SERVICE_URL
78+
});
79+
}

packages/cli-core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Core API for PowerSync CLI and plugins.
33
* Plugins (e.g. plugin-docker) import from @powersync/cli-core.
44
*/
5+
export * from './clients/accounts-client.js';
56
export * from './clients/CloudClient.js';
67
export * from './clients/SelfHostedClient.js';
78
export * from './command-types/CloudInstanceCommand.js';

packages/cli-core/src/utils/env.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
const DEFAULT_PS_MANAGEMENT_SERVICE_URL = 'https://powersync-api.journeyapps.com';
2+
const DEFAULT_PS_ACCOUNTS_HUB_SERVICE_URL = 'https://accounts.journeyapps.com';
23

34
export type ENV = {
45
_PS_MANAGEMENT_SERVICE_URL: string;
6+
_PS_ACCOUNTS_HUB_SERVICE_URL: string;
57
TOKEN?: string;
68
INSTANCE_ID?: string;
79
ORG_ID?: string;
@@ -11,6 +13,7 @@ export type ENV = {
1113

1214
export const env: ENV = {
1315
_PS_MANAGEMENT_SERVICE_URL: process.env._PS_MANAGEMENT_SERVICE_URL || DEFAULT_PS_MANAGEMENT_SERVICE_URL,
16+
_PS_ACCOUNTS_HUB_SERVICE_URL: process.env._PS_ACCOUNTS_HUB_SERVICE_URL || DEFAULT_PS_ACCOUNTS_HUB_SERVICE_URL,
1417
TOKEN: process.env.TOKEN,
1518
INSTANCE_ID: process.env.INSTANCE_ID,
1619
ORG_ID: process.env.ORG_ID,

0 commit comments

Comments
 (0)