@kinotic-ai/kinotic-cli 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -393
- package/dist/commands/create/project.d.ts +5 -1
- package/dist/commands/create/project.js +17 -11
- package/dist/commands/generate.d.ts +2 -2
- package/dist/commands/initialize.d.ts +4 -4
- package/dist/commands/login.d.ts +9 -0
- package/dist/commands/login.js +21 -0
- package/dist/commands/spawn/lint.d.ts +13 -0
- package/dist/commands/spawn/lint.js +51 -0
- package/dist/commands/synchronize.d.ts +5 -6
- package/dist/commands/synchronize.js +39 -38
- package/dist/internal/CliAuthenticator.d.ts +46 -0
- package/dist/internal/CliAuthenticator.js +211 -0
- package/dist/internal/Logger.d.ts +1 -1
- package/dist/internal/Utils.d.ts +0 -16
- package/dist/internal/Utils.js +0 -156
- package/dist/internal/spawn/FileSystemSpawnEngine.d.ts +23 -0
- package/dist/internal/spawn/FileSystemSpawnEngine.js +33 -0
- package/dist/internal/spawn/InquirerPropertyResolver.d.ts +9 -0
- package/dist/internal/spawn/InquirerPropertyResolver.js +30 -0
- package/dist/templates/entity/BaseRepository.liquid +2 -2
- package/dist/templates/spawns/project/package.json.liquid +2 -2
- package/dist/templates/spawns/project/spawn.json +10 -1
- package/package.json +19 -34
- package/bin/dev.cmd +0 -3
- package/bin/dev.js +0 -6
- package/bin/run.cmd +0 -3
- package/bin/run.js +0 -6
- package/dist/internal/spawn/SpawnConfig.d.ts +0 -31
- package/dist/internal/spawn/SpawnConfig.js +0 -12
- package/dist/internal/spawn/SpawnEngine.d.ts +0 -27
- package/dist/internal/spawn/SpawnEngine.js +0 -187
- package/oclif.manifest.json +0 -213
|
@@ -7,7 +7,7 @@ import { WebSocket } from 'ws';
|
|
|
7
7
|
import { EntityCodeGenerationService } from '../internal/EntityCodeGenerationService.js';
|
|
8
8
|
import { ProjectMigrationService } from '../internal/ProjectMigrationService.js';
|
|
9
9
|
import { resolveServer } from '../internal/state/Environment.js';
|
|
10
|
-
import {
|
|
10
|
+
import { CliAuthenticator } from '../internal/CliAuthenticator.js';
|
|
11
11
|
// This is required when running Kinotic from node
|
|
12
12
|
Object.assign(global, { WebSocket });
|
|
13
13
|
Kinotic.use(OsApiPlugin);
|
|
@@ -24,7 +24,6 @@ export class Synchronize extends Command {
|
|
|
24
24
|
server: Flags.string({ char: 's', description: 'The Kinotic server to connect to' }),
|
|
25
25
|
publish: Flags.boolean({ char: 'p', description: 'Publish each Entity after save/update' }),
|
|
26
26
|
verbose: Flags.boolean({ char: 'v', description: 'Enable verbose logging' }),
|
|
27
|
-
authHeaderFile: Flags.string({ char: 'f', description: 'JSON File containing authentication headers', required: false }),
|
|
28
27
|
dryRun: Flags.boolean({ description: 'Dry run enables verbose logging and does not save any changes to the server' }),
|
|
29
28
|
force: Flags.boolean({ description: 'Force full regeneration, ignoring incremental change detection', default: false })
|
|
30
29
|
};
|
|
@@ -40,45 +39,47 @@ export class Synchronize extends Command {
|
|
|
40
39
|
const serverConfig = await resolveServer(this.config.configDir, flags.server);
|
|
41
40
|
serverUrl = serverConfig.url;
|
|
42
41
|
}
|
|
43
|
-
if (flags.dryRun
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
if (!flags.dryRun && !(await new CliAuthenticator(serverUrl, this.config.configDir, this).connect())) {
|
|
43
|
+
this.error('Could not connect to the Kinotic Server');
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
let project = null;
|
|
47
|
+
if (!flags.dryRun) {
|
|
48
|
+
await Kinotic.applications.createApplicationIfNotExist(kinoticProjectConfig.application, '');
|
|
49
|
+
project = new Project(null, kinoticProjectConfig.application, kinoticProjectConfig.name, kinoticProjectConfig.description);
|
|
50
|
+
project.organizationId = kinoticProjectConfig.organization;
|
|
51
|
+
project.sourceOfTruth = ProjectType.TYPESCRIPT;
|
|
52
|
+
project = await Kinotic.projects.createProjectIfNotExist(project);
|
|
53
|
+
}
|
|
54
|
+
const codeGenerationService = new EntityCodeGenerationService(kinoticProjectConfig.application, kinoticProjectConfig.fileExtensionForImports, this);
|
|
55
|
+
await codeGenerationService
|
|
56
|
+
.generateAllEntities(kinoticProjectConfig, flags.verbose || flags.dryRun, async (entityInfo, services) => {
|
|
57
|
+
// combine named queries from generated services
|
|
58
|
+
const namedQueries = [];
|
|
59
|
+
for (let serviceInfo of services) {
|
|
60
|
+
namedQueries.push(...serviceInfo.namedQueries);
|
|
61
|
+
}
|
|
62
|
+
// We sync named queries first since currently the backend cache eviction logic is a little dumb
|
|
63
|
+
// i.e. The cache eviction for the EntityDefinition deletes the GraphQL schema
|
|
64
|
+
// This will evict the named query execution plan cache
|
|
65
|
+
// We want to make sure the GraphQL schema is updated after both these are updated and the EntityDefinition below
|
|
66
|
+
if (!flags.dryRun && namedQueries.length > 0) {
|
|
67
|
+
await this.synchronizeNamedQueries(kinoticProjectConfig.organization, project.id, entityInfo.entity, namedQueries);
|
|
51
68
|
}
|
|
52
|
-
const codeGenerationService = new EntityCodeGenerationService(kinoticProjectConfig.application, kinoticProjectConfig.fileExtensionForImports, this);
|
|
53
|
-
await codeGenerationService
|
|
54
|
-
.generateAllEntities(kinoticProjectConfig, flags.verbose || flags.dryRun, async (entityInfo, services) => {
|
|
55
|
-
// combine named queries from generated services
|
|
56
|
-
const namedQueries = [];
|
|
57
|
-
for (let serviceInfo of services) {
|
|
58
|
-
namedQueries.push(...serviceInfo.namedQueries);
|
|
59
|
-
}
|
|
60
|
-
// We sync named queries first since currently the backend cache eviction logic is a little dumb
|
|
61
|
-
// i.e. The cache eviction for the EntityDefinition deletes the GraphQL schema
|
|
62
|
-
// This will evict the named query execution plan cache
|
|
63
|
-
// We want to make sure the GraphQL schema is updated after both these are updated and the EntityDefinition below
|
|
64
|
-
if (!flags.dryRun && namedQueries.length > 0) {
|
|
65
|
-
await this.synchronizeNamedQueries(kinoticProjectConfig.organization, project.id, entityInfo.entity, namedQueries);
|
|
66
|
-
}
|
|
67
|
-
if (!flags.dryRun) {
|
|
68
|
-
await this.synchronizeEntity(kinoticProjectConfig.organization, project.id, entityInfo.entity, flags.publish, flags.verbose);
|
|
69
|
-
}
|
|
70
|
-
}, flags.force);
|
|
71
|
-
// Apply migrations after entity synchronization
|
|
72
69
|
if (!flags.dryRun) {
|
|
73
|
-
|
|
74
|
-
await migrationService.applyMigrations(project.id, './migrations', flags.verbose);
|
|
70
|
+
await this.synchronizeEntity(kinoticProjectConfig.organization, project.id, entityInfo.entity, flags.publish, flags.verbose);
|
|
75
71
|
}
|
|
76
|
-
|
|
72
|
+
}, flags.force);
|
|
73
|
+
// Apply migrations after entity synchronization
|
|
74
|
+
if (!flags.dryRun) {
|
|
75
|
+
const migrationService = new ProjectMigrationService(this);
|
|
76
|
+
await migrationService.applyMigrations(project.id, './migrations', flags.verbose);
|
|
77
77
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
this.log(`Synchronization Complete For application: ${kinoticProjectConfig.application}`);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
if (e instanceof Error) {
|
|
82
|
+
this.error(e.message);
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
await Kinotic.disconnect();
|
|
@@ -144,7 +145,7 @@ export class Synchronize extends Command {
|
|
|
144
145
|
const id = (organizationId + '.' + application + '.' + entityDefinitionName).toLowerCase();
|
|
145
146
|
this.log(`Synchronizing Named Queries for Entity: ${application}.${entityDefinitionName}`);
|
|
146
147
|
try {
|
|
147
|
-
const namedQueriesDefinition = new NamedQueriesDefinition(id, application, projectId, entityDefinitionName, namedQueries);
|
|
148
|
+
const namedQueriesDefinition = new NamedQueriesDefinition(id, organizationId, application, projectId, entityDefinitionName, namedQueries);
|
|
148
149
|
await namedQueriesService.save(namedQueriesDefinition);
|
|
149
150
|
}
|
|
150
151
|
catch (e) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Logger } from './Logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* CLI authentication against a Kinotic server using the OAuth 2.0 Device Authorization Grant
|
|
4
|
+
* (RFC 8628). {@link login} runs the interactive browser flow once and stores the refresh
|
|
5
|
+
* token; {@link connect} uses that stored token to open an authenticated {@link Kinotic}
|
|
6
|
+
* connection, refreshing the short-lived access token before every (re)connect.
|
|
7
|
+
*/
|
|
8
|
+
export declare class CliAuthenticator {
|
|
9
|
+
private readonly server;
|
|
10
|
+
private readonly configDir;
|
|
11
|
+
private readonly logger;
|
|
12
|
+
private refreshToken;
|
|
13
|
+
private accessToken;
|
|
14
|
+
private accessTokenExpiresAt;
|
|
15
|
+
/**
|
|
16
|
+
* @param server the server url to authenticate against
|
|
17
|
+
* @param configDir directory the rotating refresh token is persisted in
|
|
18
|
+
* @param logger sink for user-facing progress messages
|
|
19
|
+
*/
|
|
20
|
+
constructor(server: string, configDir: string, logger: Pick<Logger, 'log'>);
|
|
21
|
+
/**
|
|
22
|
+
* Runs the interactive device-authorization login and persists the refresh token, so
|
|
23
|
+
* later {@link connect} calls — this run or a future one — are non-interactive.
|
|
24
|
+
*
|
|
25
|
+
* @return true if login succeeded
|
|
26
|
+
*/
|
|
27
|
+
login(): Promise<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Opens an authenticated {@link Kinotic} connection using the stored refresh token. Fails
|
|
30
|
+
* fast — with no interactive prompt — when there are no stored credentials.
|
|
31
|
+
*
|
|
32
|
+
* @return true if the connection was established
|
|
33
|
+
*/
|
|
34
|
+
connect(): Promise<boolean>;
|
|
35
|
+
/**
|
|
36
|
+
* Returns a valid access token, refreshing it — and persisting the rotated refresh token —
|
|
37
|
+
* when it is absent or within 10s of expiry.
|
|
38
|
+
*/
|
|
39
|
+
private freshAccessToken;
|
|
40
|
+
/** Parses the server url into the host/port the gateway serves both REST and STOMP on. */
|
|
41
|
+
private parseServer;
|
|
42
|
+
/** Runs the RFC 8628 device flow: start, browser approval, then poll for tokens. */
|
|
43
|
+
private deviceLogin;
|
|
44
|
+
private loadRefreshToken;
|
|
45
|
+
private saveRefreshToken;
|
|
46
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { BearerTokenAuthProvider, ConnectionInfo, createAuthenticatedWebSocketFactory, Kinotic } from '@kinotic-ai/core';
|
|
2
|
+
import { confirm } from '@inquirer/prompts';
|
|
3
|
+
import open from 'open';
|
|
4
|
+
import pTimeout from 'p-timeout';
|
|
5
|
+
import { createStateManager } from './state/IStateManager.js';
|
|
6
|
+
/** State key the rotating refresh token is persisted under, keyed by server url. */
|
|
7
|
+
const CREDENTIALS_KEY = 'kinotic-credentials';
|
|
8
|
+
/** Per-request timeout for REST calls to the Kinotic Server. */
|
|
9
|
+
const FETCH_TIMEOUT_MS = 30_000;
|
|
10
|
+
/**
|
|
11
|
+
* CLI authentication against a Kinotic server using the OAuth 2.0 Device Authorization Grant
|
|
12
|
+
* (RFC 8628). {@link login} runs the interactive browser flow once and stores the refresh
|
|
13
|
+
* token; {@link connect} uses that stored token to open an authenticated {@link Kinotic}
|
|
14
|
+
* connection, refreshing the short-lived access token before every (re)connect.
|
|
15
|
+
*/
|
|
16
|
+
export class CliAuthenticator {
|
|
17
|
+
server;
|
|
18
|
+
configDir;
|
|
19
|
+
logger;
|
|
20
|
+
refreshToken = null;
|
|
21
|
+
accessToken = null;
|
|
22
|
+
accessTokenExpiresAt = 0;
|
|
23
|
+
/**
|
|
24
|
+
* @param server the server url to authenticate against
|
|
25
|
+
* @param configDir directory the rotating refresh token is persisted in
|
|
26
|
+
* @param logger sink for user-facing progress messages
|
|
27
|
+
*/
|
|
28
|
+
constructor(server, configDir, logger) {
|
|
29
|
+
this.server = server;
|
|
30
|
+
this.configDir = configDir;
|
|
31
|
+
this.logger = logger;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Runs the interactive device-authorization login and persists the refresh token, so
|
|
35
|
+
* later {@link connect} calls — this run or a future one — are non-interactive.
|
|
36
|
+
*
|
|
37
|
+
* @return true if login succeeded
|
|
38
|
+
*/
|
|
39
|
+
async login() {
|
|
40
|
+
const target = this.parseServer();
|
|
41
|
+
if (target === null) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const tokens = await this.deviceLogin(target.restBaseUrl);
|
|
45
|
+
if (tokens === null) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
await this.saveRefreshToken(tokens.refresh_token);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Opens an authenticated {@link Kinotic} connection using the stored refresh token. Fails
|
|
53
|
+
* fast — with no interactive prompt — when there are no stored credentials.
|
|
54
|
+
*
|
|
55
|
+
* @return true if the connection was established
|
|
56
|
+
*/
|
|
57
|
+
async connect() {
|
|
58
|
+
try {
|
|
59
|
+
const target = this.parseServer();
|
|
60
|
+
if (target === null) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
this.refreshToken = await this.loadRefreshToken();
|
|
64
|
+
if (this.refreshToken === null) {
|
|
65
|
+
this.logger.log('Not logged in. Run `kinotic login` first.');
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const connectionInfo = new ConnectionInfo();
|
|
69
|
+
connectionInfo.host = target.host;
|
|
70
|
+
connectionInfo.port = target.port;
|
|
71
|
+
connectionInfo.useSSL = target.useSSL;
|
|
72
|
+
// The CLI is a Node client: core builds the broker URL and attaches the bearer
|
|
73
|
+
// token as a WebSocket upgrade header. The supplier refreshes the access token
|
|
74
|
+
// before each (re)connect, since core consults the provider on every connect.
|
|
75
|
+
connectionInfo.webSocketFactory = createAuthenticatedWebSocketFactory({ host: target.host, port: target.port, useSSL: target.useSSL }, new BearerTokenAuthProvider(() => this.freshAccessToken(target.restBaseUrl)));
|
|
76
|
+
await pTimeout(Kinotic.connect(connectionInfo), {
|
|
77
|
+
milliseconds: 60000,
|
|
78
|
+
message: 'Connection timeout trying to connect to the Kinotic Server'
|
|
79
|
+
});
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
this.logger.log('Could not connect to the Kinotic Server: '
|
|
84
|
+
+ (e instanceof Error ? e.message : String(e)));
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Returns a valid access token, refreshing it — and persisting the rotated refresh token —
|
|
90
|
+
* when it is absent or within 10s of expiry.
|
|
91
|
+
*/
|
|
92
|
+
async freshAccessToken(restBaseUrl) {
|
|
93
|
+
if (this.accessToken !== null && Date.now() < this.accessTokenExpiresAt - 10_000) {
|
|
94
|
+
return this.accessToken;
|
|
95
|
+
}
|
|
96
|
+
const res = await fetch(restBaseUrl + '/api/auth/device/refresh', {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: { 'Content-Type': 'application/json' },
|
|
99
|
+
body: JSON.stringify({ refresh_token: this.refreshToken }),
|
|
100
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
101
|
+
});
|
|
102
|
+
if (!res.ok) {
|
|
103
|
+
throw new Error('Session expired. Run `kinotic login` again.');
|
|
104
|
+
}
|
|
105
|
+
const tokens = await res.json();
|
|
106
|
+
this.refreshToken = tokens.refresh_token;
|
|
107
|
+
this.accessToken = tokens.access_token;
|
|
108
|
+
this.accessTokenExpiresAt = Date.now() + (tokens.expires_in ?? 60) * 1000;
|
|
109
|
+
await this.saveRefreshToken(this.refreshToken);
|
|
110
|
+
return this.accessToken;
|
|
111
|
+
}
|
|
112
|
+
/** Parses the server url into the host/port the gateway serves both REST and STOMP on. */
|
|
113
|
+
parseServer() {
|
|
114
|
+
const url = new URL(this.server);
|
|
115
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
116
|
+
this.logger.log('Invalid server URL, only http and https are supported');
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const useSSL = url.protocol === 'https:';
|
|
120
|
+
// Locally the server url often points at the static web port; the gateway
|
|
121
|
+
// (REST + STOMP) always listens on 58503, so the port is overridden.
|
|
122
|
+
let port;
|
|
123
|
+
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
|
|
124
|
+
port = 58503;
|
|
125
|
+
}
|
|
126
|
+
else if (url.port) {
|
|
127
|
+
port = Number(url.port);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
port = useSSL ? 443 : 58503;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
host: url.hostname,
|
|
134
|
+
port,
|
|
135
|
+
useSSL,
|
|
136
|
+
restBaseUrl: (useSSL ? 'https' : 'http') + '://' + url.hostname + ':' + port
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** Runs the RFC 8628 device flow: start, browser approval, then poll for tokens. */
|
|
140
|
+
async deviceLogin(restBaseUrl) {
|
|
141
|
+
const startRes = await fetch(restBaseUrl + '/api/auth/device/start', {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
144
|
+
});
|
|
145
|
+
if (!startRes.ok) {
|
|
146
|
+
this.logger.log('Could not start device authorization with the Kinotic Server.');
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const start = await startRes.json();
|
|
150
|
+
this.logger.log('Authenticate your account at:');
|
|
151
|
+
this.logger.log(start.verification_uri_complete);
|
|
152
|
+
this.logger.log(`Your code is: ${start.user_code}`);
|
|
153
|
+
const answer = await confirm({ message: 'Open in browser?', default: true });
|
|
154
|
+
if (answer) {
|
|
155
|
+
await open(start.verification_uri_complete);
|
|
156
|
+
}
|
|
157
|
+
const deadline = Date.now() + start.expires_in * 1000;
|
|
158
|
+
let intervalMs = Math.max(start.interval, 1) * 1000;
|
|
159
|
+
while (Date.now() < deadline) {
|
|
160
|
+
await delay(intervalMs);
|
|
161
|
+
const tokenRes = await fetch(restBaseUrl + '/api/auth/device/token', {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: { 'Content-Type': 'application/json' },
|
|
164
|
+
body: JSON.stringify({ device_code: start.device_code }),
|
|
165
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
166
|
+
});
|
|
167
|
+
if (tokenRes.ok) {
|
|
168
|
+
return await tokenRes.json();
|
|
169
|
+
}
|
|
170
|
+
const error = await readErrorCode(tokenRes);
|
|
171
|
+
if (error === 'slow_down') {
|
|
172
|
+
intervalMs += 5000;
|
|
173
|
+
}
|
|
174
|
+
else if (error !== 'authorization_pending') {
|
|
175
|
+
this.logger.log(`Device authorization failed: ${error}`);
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
this.logger.log('Device authorization timed out before it was approved.');
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
async loadRefreshToken() {
|
|
183
|
+
const stateManager = createStateManager(this.configDir);
|
|
184
|
+
if (!(await stateManager.containsState(CREDENTIALS_KEY))) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const credentials = await stateManager.load(CREDENTIALS_KEY);
|
|
188
|
+
return credentials[this.server] ?? null;
|
|
189
|
+
}
|
|
190
|
+
async saveRefreshToken(refreshToken) {
|
|
191
|
+
const stateManager = createStateManager(this.configDir);
|
|
192
|
+
let credentials = {};
|
|
193
|
+
if (await stateManager.containsState(CREDENTIALS_KEY)) {
|
|
194
|
+
credentials = await stateManager.load(CREDENTIALS_KEY);
|
|
195
|
+
}
|
|
196
|
+
credentials[this.server] = refreshToken;
|
|
197
|
+
await stateManager.save(CREDENTIALS_KEY, credentials);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function readErrorCode(res) {
|
|
201
|
+
try {
|
|
202
|
+
const body = await res.json();
|
|
203
|
+
return body.error ?? 'unknown_error';
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return 'unknown_error';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function delay(ms) {
|
|
210
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
211
|
+
}
|
package/dist/internal/Utils.d.ts
CHANGED
|
@@ -5,23 +5,7 @@ export type GeneratedServiceInfo = {
|
|
|
5
5
|
entityServiceName: string;
|
|
6
6
|
namedQueries: FunctionDefinition[];
|
|
7
7
|
};
|
|
8
|
-
export declare class SessionMetadata {
|
|
9
|
-
sessionId: string;
|
|
10
|
-
replyToId: string;
|
|
11
|
-
constructor(sessionId: string, replyToId: string);
|
|
12
|
-
}
|
|
13
8
|
export declare function jsonStringifyReplacer(key: any, value: any): any;
|
|
14
|
-
/**
|
|
15
|
-
* Connects to the server and upgrades the session to a CLI session
|
|
16
|
-
* Currently this works by connecting and waiting for the clients session id on the event bus
|
|
17
|
-
* The cli then disconnects and reconnects using the clients' session.
|
|
18
|
-
* This will be replaced when the server supports a session upgrade command
|
|
19
|
-
* @param server the server to connect to
|
|
20
|
-
* @param logger the logger to use
|
|
21
|
-
* @param authHeadersFile path to a file containing the auth headers
|
|
22
|
-
* @return true if the session was upgraded successfully
|
|
23
|
-
*/
|
|
24
|
-
export declare function connectAndUpgradeSession(server: string, logger: Logger, authHeadersFile?: string): Promise<boolean>;
|
|
25
9
|
export type EntityInfo = {
|
|
26
10
|
exportedFromFile: string;
|
|
27
11
|
defaultExport: boolean;
|
package/dist/internal/Utils.js
CHANGED
|
@@ -1,24 +1,11 @@
|
|
|
1
|
-
import { Kinotic, Event, EventConstants, ParticipantConstants } from '@kinotic-ai/core';
|
|
2
1
|
import { ObjectC3Type } from '@kinotic-ai/idl';
|
|
3
2
|
import fs from 'fs';
|
|
4
3
|
import fsPromises from 'fs/promises';
|
|
5
|
-
import { confirm } from '@inquirer/prompts';
|
|
6
|
-
import open from 'open';
|
|
7
|
-
import pTimeout from 'p-timeout';
|
|
8
4
|
import path from 'path';
|
|
9
5
|
import { IndentationText, Node, Project } from 'ts-morph';
|
|
10
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
11
6
|
import { createConversionContext } from './converter/IConversionContext.js';
|
|
12
7
|
import { TypescriptConversionState } from './converter/typescript/TypescriptConversionState.js';
|
|
13
8
|
import { TypescriptConverterStrategy } from './converter/typescript/TypescriptConverterStrategy.js';
|
|
14
|
-
export class SessionMetadata {
|
|
15
|
-
sessionId;
|
|
16
|
-
replyToId;
|
|
17
|
-
constructor(sessionId, replyToId) {
|
|
18
|
-
this.sessionId = sessionId;
|
|
19
|
-
this.replyToId = replyToId;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
9
|
function isEmpty(value) {
|
|
23
10
|
if (value === null || value === undefined) {
|
|
24
11
|
return true;
|
|
@@ -36,134 +23,6 @@ export function jsonStringifyReplacer(key, value) {
|
|
|
36
23
|
? undefined
|
|
37
24
|
: value;
|
|
38
25
|
}
|
|
39
|
-
/**
|
|
40
|
-
* Connects to the server and upgrades the session to a CLI session
|
|
41
|
-
* Currently this works by connecting and waiting for the clients session id on the event bus
|
|
42
|
-
* The cli then disconnects and reconnects using the clients' session.
|
|
43
|
-
* This will be replaced when the server supports a session upgrade command
|
|
44
|
-
* @param server the server to connect to
|
|
45
|
-
* @param logger the logger to use
|
|
46
|
-
* @param authHeadersFile path to a file containing the auth headers
|
|
47
|
-
* @return true if the session was upgraded successfully
|
|
48
|
-
*/
|
|
49
|
-
export async function connectAndUpgradeSession(server, logger, authHeadersFile) {
|
|
50
|
-
try {
|
|
51
|
-
const serverURL = new URL(server);
|
|
52
|
-
if (serverURL.protocol !== 'http:' && serverURL.protocol !== 'https:') {
|
|
53
|
-
logger.log('Invalid server URL, only http and https are supported');
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
let connectionInfo = { host: '' };
|
|
57
|
-
if (serverURL.hostname === 'localhost' || serverURL.hostname === '127.0.0.1') {
|
|
58
|
-
connectionInfo.host = serverURL.hostname;
|
|
59
|
-
connectionInfo.port = 58503;
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
connectionInfo.host = serverURL.hostname;
|
|
63
|
-
if (serverURL.protocol === 'https:') {
|
|
64
|
-
connectionInfo.useSSL = true;
|
|
65
|
-
connectionInfo.port = 443;
|
|
66
|
-
}
|
|
67
|
-
if (serverURL.port) {
|
|
68
|
-
connectionInfo.port = Number(serverURL.port);
|
|
69
|
-
}
|
|
70
|
-
else if (!connectionInfo.useSSL) {
|
|
71
|
-
connectionInfo.port = 58503;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (authHeadersFile) {
|
|
75
|
-
if (!fs.existsSync(authHeadersFile)) {
|
|
76
|
-
logger.log(`Authentication header file ${authHeadersFile} does not exist. Please provide a valid file.`);
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
logger.log(`Authentication header file ${authHeadersFile} loaded successfully. Using authentication headers to connect to the Kinotic Server.`);
|
|
81
|
-
}
|
|
82
|
-
const authHeaders = JSON.parse(fs.readFileSync(authHeadersFile, 'utf8'));
|
|
83
|
-
connectionInfo.connectHeaders = authHeaders;
|
|
84
|
-
const connectedInfo = await pTimeout(Kinotic.connect(connectionInfo), {
|
|
85
|
-
milliseconds: 60000,
|
|
86
|
-
message: 'Connection timeout trying to connect to the Kinotic Server'
|
|
87
|
-
});
|
|
88
|
-
if (connectedInfo) {
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
connectionInfo.connectHeaders = {
|
|
97
|
-
login: ParticipantConstants.CLI_PARTICIPANT_ID
|
|
98
|
-
};
|
|
99
|
-
const connectedInfo = await pTimeout(Kinotic.connect(connectionInfo), {
|
|
100
|
-
milliseconds: 60000,
|
|
101
|
-
message: 'Connection timeout trying to connect to the Kinotic Server'
|
|
102
|
-
});
|
|
103
|
-
if (connectedInfo) {
|
|
104
|
-
// This works because any client can subscribe to a destination that is scoped to the connectedInfo.replyToId
|
|
105
|
-
const scope = connectedInfo.replyToId + ':' + uuidv4();
|
|
106
|
-
const url = server + (server.endsWith('/') ? '' : '/') + '#/sessionUpgrade/' + encodeURIComponent(scope);
|
|
107
|
-
logger.log('Authenticate your account at:');
|
|
108
|
-
logger.log(url);
|
|
109
|
-
const answer = await confirm({
|
|
110
|
-
message: 'Open in browser?',
|
|
111
|
-
default: true,
|
|
112
|
-
});
|
|
113
|
-
if (answer) {
|
|
114
|
-
await open(url);
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
logger.log('Browser will not be opened. You must authenticate your account before continuing.');
|
|
118
|
-
}
|
|
119
|
-
const sessionMetadata = await receiveSessionId(scope);
|
|
120
|
-
await Kinotic.disconnect();
|
|
121
|
-
connectionInfo.connectHeaders = {
|
|
122
|
-
session: sessionMetadata.sessionId
|
|
123
|
-
};
|
|
124
|
-
// Provide this so the continuum client will use the same replyToId as the session
|
|
125
|
-
connectionInfo.connectHeaders[EventConstants.REPLY_TO_ID_HEADER] = sessionMetadata.replyToId;
|
|
126
|
-
await Kinotic.connect(connectionInfo);
|
|
127
|
-
logger.log('Authenticated successfully\n');
|
|
128
|
-
return true;
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
logger.log("Could not connect to the Kinotic Server. Please check the server is running and the URL is correct.");
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
catch (e) {
|
|
137
|
-
logger.log("Could not connect to the Kinotic Server. Please check the server is running and the URL is correct.", e);
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
function receiveSessionId(scope) {
|
|
142
|
-
const subscribeCRI = EventConstants.SERVICE_DESTINATION_PREFIX + scope + '@continuum.cli.SessionUpgradeService';
|
|
143
|
-
return new Promise((resolve, reject) => {
|
|
144
|
-
const subscription = Kinotic.eventBus.observe(subscribeCRI).subscribe((value) => {
|
|
145
|
-
// send reply to user
|
|
146
|
-
const replyTo = value.getHeader(EventConstants.REPLY_TO_HEADER);
|
|
147
|
-
if (replyTo) {
|
|
148
|
-
const replyEvent = new Event(replyTo);
|
|
149
|
-
const correlationId = value.getHeader(EventConstants.CORRELATION_ID_HEADER);
|
|
150
|
-
if (correlationId) {
|
|
151
|
-
replyEvent.setHeader(EventConstants.CORRELATION_ID_HEADER, correlationId);
|
|
152
|
-
}
|
|
153
|
-
replyEvent.setHeader(EventConstants.CONTENT_TYPE_HEADER, EventConstants.CONTENT_JSON);
|
|
154
|
-
Kinotic.eventBus.send(replyEvent);
|
|
155
|
-
}
|
|
156
|
-
subscription.unsubscribe();
|
|
157
|
-
const jsonObj = JSON.parse(value.getDataString());
|
|
158
|
-
if (jsonObj?.length > 0) {
|
|
159
|
-
resolve(jsonObj[0]);
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
reject('No Session Id found in data');
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
26
|
function getEntityDecoratorIfExists(node) {
|
|
168
27
|
if (Node.isClassDeclaration(node)) {
|
|
169
28
|
return node.getDecorator('Entity');
|
|
@@ -182,21 +41,6 @@ export function createTsMorphProject() {
|
|
|
182
41
|
manipulationSettings: {
|
|
183
42
|
indentationText: IndentationText.TwoSpaces
|
|
184
43
|
}
|
|
185
|
-
// compilerOptions: {
|
|
186
|
-
// target: ScriptTarget.ES2020,
|
|
187
|
-
// useDefineForClassFields: true,
|
|
188
|
-
// module: ModuleKind.ES2020,
|
|
189
|
-
// lib: ["ES2020"],
|
|
190
|
-
// skipLibCheck: true,
|
|
191
|
-
// downlevelIteration: true,
|
|
192
|
-
// emitDecoratorMetadata: true,
|
|
193
|
-
// experimentalDecorators: true,
|
|
194
|
-
// esModuleInterop: true,
|
|
195
|
-
// moduleResolution: ModuleResolutionKind.NodeNext,
|
|
196
|
-
// resolveJsonModule: true,
|
|
197
|
-
// isolatedModules: true,
|
|
198
|
-
// noEmit: true,
|
|
199
|
-
// }
|
|
200
44
|
});
|
|
201
45
|
}
|
|
202
46
|
/**
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SpawnResolver } from './SpawnResolver.js';
|
|
2
|
+
/**
|
|
3
|
+
* Renders Spawns bundled with the CLI to the local filesystem, prompting on
|
|
4
|
+
* the terminal for any properties the spawn requires that were not provided in
|
|
5
|
+
* the context. Resolves a spawn by name to its bundled directory and delegates
|
|
6
|
+
* the disk load/render/write (and its directory-traversal guards) to
|
|
7
|
+
* {@link NodeSpawnRenderer}.
|
|
8
|
+
*/
|
|
9
|
+
export declare class FileSystemSpawnEngine {
|
|
10
|
+
private renderer;
|
|
11
|
+
private spawnResolver;
|
|
12
|
+
constructor(resolver: SpawnResolver);
|
|
13
|
+
/**
|
|
14
|
+
* Renders the named spawn into {@code destination}, which must not exist yet.
|
|
15
|
+
*
|
|
16
|
+
* @param spawn the name of the spawn to render. This is the name of the directory containing the spawn.json
|
|
17
|
+
* @param destination the target directory where rendered files will be written
|
|
18
|
+
* @param context the values to be provided to the templates while rendering
|
|
19
|
+
* @return a promise containing all the original values plus any added during rendering
|
|
20
|
+
*/
|
|
21
|
+
renderSpawn(spawn: string, destination: string, context?: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
22
|
+
}
|
|
23
|
+
export declare const fileSystemSpawnEngine: FileSystemSpawnEngine;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { NodeSpawnRenderer } from '@kinotic-ai/spawn/node';
|
|
2
|
+
import { InquirerPropertyResolver } from './InquirerPropertyResolver.js';
|
|
3
|
+
import { spawnResolver } from './SpawnResolver.js';
|
|
4
|
+
/**
|
|
5
|
+
* Renders Spawns bundled with the CLI to the local filesystem, prompting on
|
|
6
|
+
* the terminal for any properties the spawn requires that were not provided in
|
|
7
|
+
* the context. Resolves a spawn by name to its bundled directory and delegates
|
|
8
|
+
* the disk load/render/write (and its directory-traversal guards) to
|
|
9
|
+
* {@link NodeSpawnRenderer}.
|
|
10
|
+
*/
|
|
11
|
+
export class FileSystemSpawnEngine {
|
|
12
|
+
renderer = new NodeSpawnRenderer();
|
|
13
|
+
spawnResolver;
|
|
14
|
+
constructor(resolver) {
|
|
15
|
+
this.spawnResolver = resolver;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Renders the named spawn into {@code destination}, which must not exist yet.
|
|
19
|
+
*
|
|
20
|
+
* @param spawn the name of the spawn to render. This is the name of the directory containing the spawn.json
|
|
21
|
+
* @param destination the target directory where rendered files will be written
|
|
22
|
+
* @param context the values to be provided to the templates while rendering
|
|
23
|
+
* @return a promise containing all the original values plus any added during rendering
|
|
24
|
+
*/
|
|
25
|
+
async renderSpawn(spawn, destination, context) {
|
|
26
|
+
const source = await this.spawnResolver.resolveSpawn(spawn);
|
|
27
|
+
return this.renderer.render(source, destination, {
|
|
28
|
+
context,
|
|
29
|
+
propertyResolver: new InquirerPropertyResolver()
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export const fileSystemSpawnEngine = new FileSystemSpawnEngine(spawnResolver);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PropertyResolver, PropertySchema } from '@kinotic-ai/spawn';
|
|
2
|
+
/**
|
|
3
|
+
* Prompts the user on the terminal for spawn properties missing from the
|
|
4
|
+
* render context, choosing the prompt type from the property's schema.
|
|
5
|
+
*/
|
|
6
|
+
export declare class InquirerPropertyResolver implements PropertyResolver {
|
|
7
|
+
private hasPrompted;
|
|
8
|
+
resolve(key: string, schema: PropertySchema, message: string, defaultValue?: unknown): Promise<unknown>;
|
|
9
|
+
}
|