@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.
Files changed (33) hide show
  1. package/README.md +1 -393
  2. package/dist/commands/create/project.d.ts +5 -1
  3. package/dist/commands/create/project.js +17 -11
  4. package/dist/commands/generate.d.ts +2 -2
  5. package/dist/commands/initialize.d.ts +4 -4
  6. package/dist/commands/login.d.ts +9 -0
  7. package/dist/commands/login.js +21 -0
  8. package/dist/commands/spawn/lint.d.ts +13 -0
  9. package/dist/commands/spawn/lint.js +51 -0
  10. package/dist/commands/synchronize.d.ts +5 -6
  11. package/dist/commands/synchronize.js +39 -38
  12. package/dist/internal/CliAuthenticator.d.ts +46 -0
  13. package/dist/internal/CliAuthenticator.js +211 -0
  14. package/dist/internal/Logger.d.ts +1 -1
  15. package/dist/internal/Utils.d.ts +0 -16
  16. package/dist/internal/Utils.js +0 -156
  17. package/dist/internal/spawn/FileSystemSpawnEngine.d.ts +23 -0
  18. package/dist/internal/spawn/FileSystemSpawnEngine.js +33 -0
  19. package/dist/internal/spawn/InquirerPropertyResolver.d.ts +9 -0
  20. package/dist/internal/spawn/InquirerPropertyResolver.js +30 -0
  21. package/dist/templates/entity/BaseRepository.liquid +2 -2
  22. package/dist/templates/spawns/project/package.json.liquid +2 -2
  23. package/dist/templates/spawns/project/spawn.json +10 -1
  24. package/package.json +19 -34
  25. package/bin/dev.cmd +0 -3
  26. package/bin/dev.js +0 -6
  27. package/bin/run.cmd +0 -3
  28. package/bin/run.js +0 -6
  29. package/dist/internal/spawn/SpawnConfig.d.ts +0 -31
  30. package/dist/internal/spawn/SpawnConfig.js +0 -12
  31. package/dist/internal/spawn/SpawnEngine.d.ts +0 -27
  32. package/dist/internal/spawn/SpawnEngine.js +0 -187
  33. 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 { connectAndUpgradeSession } from '../internal/Utils.js';
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 || await connectAndUpgradeSession(serverUrl, this, flags.authHeaderFile)) {
44
- try {
45
- let project = null;
46
- if (!flags.dryRun) {
47
- await Kinotic.applications.createApplicationIfNotExist(kinoticProjectConfig.application, '');
48
- project = new Project(null, kinoticProjectConfig.application, kinoticProjectConfig.name, kinoticProjectConfig.description);
49
- project.sourceOfTruth = ProjectType.TYPESCRIPT;
50
- project = await Kinotic.projects.createProjectIfNotExist(project);
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
- const migrationService = new ProjectMigrationService(this);
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
- this.log(`Synchronization Complete For application: ${kinoticProjectConfig.application}`);
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
- catch (e) {
79
- if (e instanceof Error) {
80
- this.error(e.message);
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
+ }
@@ -1,4 +1,4 @@
1
- import { PrettyPrintableError } from '@oclif/core/lib/errors';
1
+ import { PrettyPrintableError } from '@oclif/core/errors';
2
2
  export type Logger = {
3
3
  log: (message?: string, ...args: any[]) => void;
4
4
  warn(input: string | Error): string | Error;
@@ -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;
@@ -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
+ }