@kinotic-ai/kinotic-cli 2.3.0 → 3.1.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/bin/dev.cmd +3 -0
- package/bin/dev.js +6 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +6 -0
- 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 +1 -1
- package/dist/commands/spawn/lint.d.ts +13 -0
- package/dist/commands/spawn/lint.js +51 -0
- package/dist/commands/synchronize.d.ts +5 -5
- package/dist/commands/synchronize.js +1 -1
- package/dist/internal/CliAuthenticator.js +9 -15
- package/dist/internal/Logger.d.ts +1 -1
- package/dist/internal/Utils.js +0 -15
- 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/spawns/project/package.json.liquid +2 -2
- package/dist/templates/spawns/project/spawn.json +10 -1
- package/package.json +15 -10
- 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/bin/dev.cmd
ADDED
package/bin/dev.js
ADDED
package/bin/run.cmd
ADDED
package/bin/run.js
ADDED
|
@@ -3,7 +3,11 @@ export declare class Project extends Command {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static args: {
|
|
6
|
-
name: import("@oclif/core/
|
|
6
|
+
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
organization: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
application: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
11
|
};
|
|
8
12
|
run(): Promise<void>;
|
|
9
13
|
private renderProjectModule;
|
|
@@ -1,27 +1,36 @@
|
|
|
1
|
-
import { Args, Command } from '@oclif/core';
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
2
|
import { input, select } from '@inquirer/prompts';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import process from 'node:process';
|
|
7
|
-
import {
|
|
7
|
+
import { assertPathWithin } from '@kinotic-ai/spawn/node';
|
|
8
|
+
import { fileSystemSpawnEngine } from '../../internal/spawn/FileSystemSpawnEngine.js';
|
|
8
9
|
import { createFrontEnd } from '../../internal/CommandHelper.js';
|
|
9
10
|
export class Project extends Command {
|
|
10
11
|
static description = 'Creates a Kinotic Project';
|
|
11
12
|
static examples = [
|
|
12
|
-
'$ kinotic create project MyProjectName',
|
|
13
|
+
'$ kinotic create project MyProjectName --organization acme --application acme-app',
|
|
13
14
|
];
|
|
14
15
|
static args = {
|
|
15
16
|
name: Args.string({ description: 'The name for the project', required: true })
|
|
16
17
|
};
|
|
18
|
+
static flags = {
|
|
19
|
+
organization: Flags.string({ char: 'o', description: 'The organization that owns the project', required: true }),
|
|
20
|
+
application: Flags.string({ char: 'a', description: 'The application the project belongs to', required: true })
|
|
21
|
+
};
|
|
17
22
|
async run() {
|
|
18
|
-
const { args } = await this.parse(Project);
|
|
23
|
+
const { args, flags } = await this.parse(Project);
|
|
19
24
|
const projectDir = path.resolve(args.name);
|
|
20
|
-
let context = {
|
|
25
|
+
let context = {
|
|
26
|
+
projectName: args.name,
|
|
27
|
+
organization: flags.organization,
|
|
28
|
+
application: flags.application
|
|
29
|
+
};
|
|
21
30
|
this.log(chalk.cyan('Creating Kinotic Project'));
|
|
22
31
|
const spinner = ora('Generating project...').start();
|
|
23
32
|
try {
|
|
24
|
-
context =
|
|
33
|
+
context = await fileSystemSpawnEngine.renderSpawn('project', projectDir, context);
|
|
25
34
|
spinner.succeed(chalk.green('Project generated'));
|
|
26
35
|
}
|
|
27
36
|
catch (err) {
|
|
@@ -53,10 +62,7 @@ export class Project extends Command {
|
|
|
53
62
|
} while (choice !== 'Quit');
|
|
54
63
|
}
|
|
55
64
|
async renderProjectModule(spawn, name, projectDir, context) {
|
|
56
|
-
const dir =
|
|
57
|
-
|
|
58
|
-
throw new Error(`Module dir ${dir} must be within ${projectDir}`);
|
|
59
|
-
}
|
|
60
|
-
await spawnEngine.renderSpawn(spawn, dir, context);
|
|
65
|
+
const dir = assertPathWithin(projectDir, name);
|
|
66
|
+
await fileSystemSpawnEngine.renderSpawn(spawn, dir, context);
|
|
61
67
|
}
|
|
62
68
|
}
|
|
@@ -4,8 +4,8 @@ export declare class Generate extends Command {
|
|
|
4
4
|
static description: string;
|
|
5
5
|
static examples: string[];
|
|
6
6
|
static flags: {
|
|
7
|
-
verbose: import("@oclif/core/
|
|
8
|
-
force: import("@oclif/core/
|
|
7
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
9
|
};
|
|
10
10
|
run(): Promise<void>;
|
|
11
11
|
logVerbose(message: string | (() => string), verbose: boolean): void;
|
|
@@ -4,10 +4,10 @@ export declare class Initialize extends Command {
|
|
|
4
4
|
static description: string;
|
|
5
5
|
static examples: string[];
|
|
6
6
|
static flags: {
|
|
7
|
-
application: import("@oclif/core/
|
|
8
|
-
entities: import("@oclif/core/
|
|
9
|
-
repository: import("@oclif/core/
|
|
10
|
-
mirror: import("@oclif/core/
|
|
7
|
+
application: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
entities: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
repository: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
mirror: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
};
|
|
12
12
|
run(): Promise<void>;
|
|
13
13
|
}
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export declare class Login extends Command {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
-
server: import("@oclif/core/
|
|
6
|
+
server: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
7
|
};
|
|
8
8
|
run(): Promise<void>;
|
|
9
9
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export declare class Lint extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
dir: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
fix: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
private applyFix;
|
|
13
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { lintSpawnDir } from '@kinotic-ai/spawn/node';
|
|
6
|
+
export class Lint extends Command {
|
|
7
|
+
static description = 'Reports variables used in a spawn template that are not declared in its spawn.json';
|
|
8
|
+
static examples = [
|
|
9
|
+
'$ kinotic spawn lint',
|
|
10
|
+
'$ kinotic spawn lint ./my-template --fix',
|
|
11
|
+
];
|
|
12
|
+
static args = {
|
|
13
|
+
dir: Args.string({ description: 'The spawn template directory', default: '.' })
|
|
14
|
+
};
|
|
15
|
+
static flags = {
|
|
16
|
+
fix: Flags.boolean({ description: 'Add stub propertySchema entries for undeclared variables' })
|
|
17
|
+
};
|
|
18
|
+
async run() {
|
|
19
|
+
const { args, flags } = await this.parse(Lint);
|
|
20
|
+
const dir = path.resolve(args.dir);
|
|
21
|
+
const { undeclared } = await lintSpawnDir(dir);
|
|
22
|
+
if (undeclared.length === 0) {
|
|
23
|
+
this.log(chalk.green('✓ spawn.json declares everything the templates use'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
this.log(chalk.yellow('Undeclared variables used in templates:'));
|
|
27
|
+
for (const variable of undeclared) {
|
|
28
|
+
this.log(` ${chalk.bold(variable.name)} ${chalk.dim(variable.files.join(', '))}`);
|
|
29
|
+
}
|
|
30
|
+
if (flags.fix) {
|
|
31
|
+
this.applyFix(dir, undeclared.map(v => v.name));
|
|
32
|
+
this.log(chalk.green(`Added ${undeclared.length} stub(s) to spawn.json — fill in description/default.`));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.error('spawn.json is missing declarations (run with --fix to add stubs)', { exit: 1 });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Adds a string-typed stub to the root spawn.json's propertySchema for each
|
|
39
|
+
// undeclared name, leaving existing entries untouched.
|
|
40
|
+
applyFix(dir, names) {
|
|
41
|
+
const spawnJsonPath = path.join(dir, 'spawn.json');
|
|
42
|
+
const config = fs.existsSync(spawnJsonPath)
|
|
43
|
+
? JSON.parse(fs.readFileSync(spawnJsonPath, { encoding: 'utf8' }))
|
|
44
|
+
: {};
|
|
45
|
+
config.propertySchema ??= {};
|
|
46
|
+
for (const name of names) {
|
|
47
|
+
config.propertySchema[name] ??= { type: 'string', description: 'TODO' };
|
|
48
|
+
}
|
|
49
|
+
fs.writeFileSync(spawnJsonPath, JSON.stringify(config, null, 2) + '\n');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -4,11 +4,11 @@ export declare class Synchronize extends Command {
|
|
|
4
4
|
static description: string;
|
|
5
5
|
static examples: string[];
|
|
6
6
|
static flags: {
|
|
7
|
-
server: import("@oclif/core/
|
|
8
|
-
publish: import("@oclif/core/
|
|
9
|
-
verbose: import("@oclif/core/
|
|
10
|
-
dryRun: import("@oclif/core/
|
|
11
|
-
force: import("@oclif/core/
|
|
7
|
+
server: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
publish: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
dryRun: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
12
|
};
|
|
13
13
|
run(): Promise<void>;
|
|
14
14
|
logVerbose(message: string | (() => string), verbose: boolean): void;
|
|
@@ -145,7 +145,7 @@ export class Synchronize extends Command {
|
|
|
145
145
|
const id = (organizationId + '.' + application + '.' + entityDefinitionName).toLowerCase();
|
|
146
146
|
this.log(`Synchronizing Named Queries for Entity: ${application}.${entityDefinitionName}`);
|
|
147
147
|
try {
|
|
148
|
-
const namedQueriesDefinition = new NamedQueriesDefinition(id, application, projectId, entityDefinitionName, namedQueries);
|
|
148
|
+
const namedQueriesDefinition = new NamedQueriesDefinition(id, organizationId, application, projectId, entityDefinitionName, namedQueries);
|
|
149
149
|
await namedQueriesService.save(namedQueriesDefinition);
|
|
150
150
|
}
|
|
151
151
|
catch (e) {
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { ConnectionInfo, Kinotic } from '@kinotic-ai/core';
|
|
1
|
+
import { BearerTokenAuthProvider, ConnectionInfo, createAuthenticatedWebSocketFactory, Kinotic } from '@kinotic-ai/core';
|
|
2
2
|
import { confirm } from '@inquirer/prompts';
|
|
3
3
|
import open from 'open';
|
|
4
4
|
import pTimeout from 'p-timeout';
|
|
5
|
-
import { WebSocket } from 'ws';
|
|
6
5
|
import { createStateManager } from './state/IStateManager.js';
|
|
7
6
|
/** State key the rotating refresh token is persisted under, keyed by server url. */
|
|
8
7
|
const CREDENTIALS_KEY = 'kinotic-credentials';
|
|
@@ -70,14 +69,10 @@ export class CliAuthenticator {
|
|
|
70
69
|
connectionInfo.host = target.host;
|
|
71
70
|
connectionInfo.port = target.port;
|
|
72
71
|
connectionInfo.useSSL = target.useSSL;
|
|
73
|
-
// The CLI is a Node client:
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return new WebSocket(target.wsUrl, {
|
|
78
|
-
headers: { Authorization: 'Bearer ' + token }
|
|
79
|
-
});
|
|
80
|
-
};
|
|
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)));
|
|
81
76
|
await pTimeout(Kinotic.connect(connectionInfo), {
|
|
82
77
|
milliseconds: 60000,
|
|
83
78
|
message: 'Connection timeout trying to connect to the Kinotic Server'
|
|
@@ -98,7 +93,7 @@ export class CliAuthenticator {
|
|
|
98
93
|
if (this.accessToken !== null && Date.now() < this.accessTokenExpiresAt - 10_000) {
|
|
99
94
|
return this.accessToken;
|
|
100
95
|
}
|
|
101
|
-
const res = await fetch(restBaseUrl + '/api/
|
|
96
|
+
const res = await fetch(restBaseUrl + '/api/auth/device/refresh', {
|
|
102
97
|
method: 'POST',
|
|
103
98
|
headers: { 'Content-Type': 'application/json' },
|
|
104
99
|
body: JSON.stringify({ refresh_token: this.refreshToken }),
|
|
@@ -138,13 +133,12 @@ export class CliAuthenticator {
|
|
|
138
133
|
host: url.hostname,
|
|
139
134
|
port,
|
|
140
135
|
useSSL,
|
|
141
|
-
restBaseUrl: (useSSL ? 'https' : 'http') + '://' + url.hostname + ':' + port
|
|
142
|
-
wsUrl: (useSSL ? 'wss' : 'ws') + '://' + url.hostname + ':' + port + '/v1'
|
|
136
|
+
restBaseUrl: (useSSL ? 'https' : 'http') + '://' + url.hostname + ':' + port
|
|
143
137
|
};
|
|
144
138
|
}
|
|
145
139
|
/** Runs the RFC 8628 device flow: start, browser approval, then poll for tokens. */
|
|
146
140
|
async deviceLogin(restBaseUrl) {
|
|
147
|
-
const startRes = await fetch(restBaseUrl + '/api/
|
|
141
|
+
const startRes = await fetch(restBaseUrl + '/api/auth/device/start', {
|
|
148
142
|
method: 'POST',
|
|
149
143
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
150
144
|
});
|
|
@@ -164,7 +158,7 @@ export class CliAuthenticator {
|
|
|
164
158
|
let intervalMs = Math.max(start.interval, 1) * 1000;
|
|
165
159
|
while (Date.now() < deadline) {
|
|
166
160
|
await delay(intervalMs);
|
|
167
|
-
const tokenRes = await fetch(restBaseUrl + '/api/
|
|
161
|
+
const tokenRes = await fetch(restBaseUrl + '/api/auth/device/token', {
|
|
168
162
|
method: 'POST',
|
|
169
163
|
headers: { 'Content-Type': 'application/json' },
|
|
170
164
|
body: JSON.stringify({ device_code: start.device_code }),
|
package/dist/internal/Utils.js
CHANGED
|
@@ -41,21 +41,6 @@ export function createTsMorphProject() {
|
|
|
41
41
|
manipulationSettings: {
|
|
42
42
|
indentationText: IndentationText.TwoSpaces
|
|
43
43
|
}
|
|
44
|
-
// compilerOptions: {
|
|
45
|
-
// target: ScriptTarget.ES2020,
|
|
46
|
-
// useDefineForClassFields: true,
|
|
47
|
-
// module: ModuleKind.ES2020,
|
|
48
|
-
// lib: ["ES2020"],
|
|
49
|
-
// skipLibCheck: true,
|
|
50
|
-
// downlevelIteration: true,
|
|
51
|
-
// emitDecoratorMetadata: true,
|
|
52
|
-
// experimentalDecorators: true,
|
|
53
|
-
// esModuleInterop: true,
|
|
54
|
-
// moduleResolution: ModuleResolutionKind.NodeNext,
|
|
55
|
-
// resolveJsonModule: true,
|
|
56
|
-
// isolatedModules: true,
|
|
57
|
-
// noEmit: true,
|
|
58
|
-
// }
|
|
59
44
|
});
|
|
60
45
|
}
|
|
61
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
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { confirm, input, number, select } from '@inquirer/prompts';
|
|
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 class InquirerPropertyResolver {
|
|
7
|
+
hasPrompted = false;
|
|
8
|
+
async resolve(key, schema, message, defaultValue) {
|
|
9
|
+
if (!this.hasPrompted) {
|
|
10
|
+
console.log('Please provide the following...\n');
|
|
11
|
+
this.hasPrompted = true;
|
|
12
|
+
}
|
|
13
|
+
if (schema.type === 'boolean') {
|
|
14
|
+
return confirm({ message, default: typeof defaultValue === 'boolean' ? defaultValue : undefined });
|
|
15
|
+
}
|
|
16
|
+
else if (schema.enum) {
|
|
17
|
+
return select({
|
|
18
|
+
message,
|
|
19
|
+
choices: schema.enum.map((v) => ({ value: v })),
|
|
20
|
+
default: defaultValue !== undefined ? String(defaultValue) : undefined
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
else if (schema.type === 'number' || schema.type === 'integer') {
|
|
24
|
+
return number({ message, default: typeof defaultValue === 'number' ? defaultValue : undefined });
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
return input({ message, default: typeof defaultValue === 'string' ? defaultValue : undefined });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"typescript": "^5.9.3"
|
|
13
13
|
},
|
|
14
14
|
"catalog": {
|
|
15
|
-
"@kinotic-ai/core": "{{
|
|
16
|
-
"@kinotic-ai/persistence": "{{
|
|
15
|
+
"@kinotic-ai/core": "{{ kinoticCoreVersion }}",
|
|
16
|
+
"@kinotic-ai/persistence": "{{ kinoticPersistenceVersion }}"
|
|
17
17
|
},
|
|
18
18
|
"workspaces": [
|
|
19
19
|
"packages/*"
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"globals": {
|
|
3
|
-
"
|
|
3
|
+
"kinoticCoreVersion": "^1.9.1",
|
|
4
|
+
"kinoticPersistenceVersion": "^2.0.0"
|
|
4
5
|
},
|
|
5
6
|
"propertySchema":{
|
|
6
7
|
"projectName": {
|
|
7
8
|
"type": "string",
|
|
8
9
|
"description": "The name of the project you want to create"
|
|
10
|
+
},
|
|
11
|
+
"organization": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"description": "The organization that owns the project"
|
|
14
|
+
},
|
|
15
|
+
"application": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "The application the project belongs to"
|
|
9
18
|
}
|
|
10
19
|
}
|
|
11
20
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kinotic-ai/kinotic-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Kinotic CLI provides the ability to build, deploy, and manage Kinotic applications that run on the Kinotic OS.",
|
|
5
5
|
"author": "Kinotic Developers",
|
|
6
6
|
"bin": {
|
|
7
7
|
"kinotic": "./bin/run.js"
|
|
8
8
|
},
|
|
9
9
|
"homepage": "https://kinotic-ai",
|
|
10
|
-
"repository":
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/kinotic-ai/kinotic"
|
|
13
|
+
},
|
|
11
14
|
"license": "Elastic License 2.0\"",
|
|
12
15
|
"main": "dist/index.js",
|
|
13
16
|
"type": "module",
|
|
@@ -18,29 +21,28 @@
|
|
|
18
21
|
],
|
|
19
22
|
"dependencies": {
|
|
20
23
|
"@inquirer/prompts": "^8.3.2",
|
|
21
|
-
"@kinotic-ai/core": "^1.
|
|
24
|
+
"@kinotic-ai/core": "^3.1.0",
|
|
22
25
|
"@kinotic-ai/idl": "^1.0.9",
|
|
23
|
-
"@kinotic-ai/os-api": "^1.
|
|
24
|
-
"@kinotic-ai/persistence": "^1.
|
|
26
|
+
"@kinotic-ai/os-api": "^3.1.0",
|
|
27
|
+
"@kinotic-ai/persistence": "^3.1.0",
|
|
28
|
+
"@kinotic-ai/spawn": "^0.5.0",
|
|
25
29
|
"@oclif/core": "^4.9.0",
|
|
26
30
|
"c12": "^3.3.3",
|
|
27
31
|
"chalk": "^5.6.2",
|
|
28
32
|
"execa": "^9.6.1",
|
|
29
33
|
"glob": "^13.0.6",
|
|
30
34
|
"graphql": "^16.13.1",
|
|
31
|
-
"liquidjs": "10.
|
|
35
|
+
"liquidjs": "10.27.0",
|
|
32
36
|
"open": "^11.0.0",
|
|
33
37
|
"ora": "^9.3.0",
|
|
34
38
|
"p-timeout": "^7.0.1",
|
|
35
39
|
"radash": "^12.1.1",
|
|
36
|
-
"reflect-metadata": "^0.2.2",
|
|
37
40
|
"simple-git": "3.36.0",
|
|
38
41
|
"terminal-link": "^5.0.0",
|
|
39
42
|
"ts-morph": "^27.0.2",
|
|
40
43
|
"uuid": "^13.0.0",
|
|
41
44
|
"ws": "^8.19.0",
|
|
42
|
-
"yaml": "2.8.3"
|
|
43
|
-
"zod": "^4.3.6"
|
|
45
|
+
"yaml": "2.8.3"
|
|
44
46
|
},
|
|
45
47
|
"devDependencies": {
|
|
46
48
|
"@types/chai": "^5",
|
|
@@ -54,9 +56,9 @@
|
|
|
54
56
|
"eslint-config-prettier": "^10",
|
|
55
57
|
"mocha": "^11",
|
|
56
58
|
"shx": "^0.4.0",
|
|
59
|
+
"tsc-alias": "^1.8.16",
|
|
57
60
|
"tslib": "^2.8.1",
|
|
58
61
|
"tsx": "^4.21.0",
|
|
59
|
-
"tsc-alias": "^1.8.16",
|
|
60
62
|
"typescript": "^5.9.3"
|
|
61
63
|
},
|
|
62
64
|
"oclif": {
|
|
@@ -67,6 +69,9 @@
|
|
|
67
69
|
"topics": {
|
|
68
70
|
"create": {
|
|
69
71
|
"description": "Creates Kinotic projects, libraries, and applications"
|
|
72
|
+
},
|
|
73
|
+
"spawn": {
|
|
74
|
+
"description": "Works with Kinotic spawn templates"
|
|
70
75
|
}
|
|
71
76
|
}
|
|
72
77
|
},
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
export declare const PropertySchemaSchema: z.ZodObject<{
|
|
3
|
-
type: z.ZodOptional<z.ZodEnum<{
|
|
4
|
-
string: "string";
|
|
5
|
-
number: "number";
|
|
6
|
-
boolean: "boolean";
|
|
7
|
-
integer: "integer";
|
|
8
|
-
}>>;
|
|
9
|
-
description: z.ZodOptional<z.ZodString>;
|
|
10
|
-
default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
11
|
-
enum: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
12
|
-
}, z.core.$strip>;
|
|
13
|
-
export declare const SpawnConfigSchema: z.ZodObject<{
|
|
14
|
-
inherits: z.ZodOptional<z.ZodString>;
|
|
15
|
-
globals: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
16
|
-
propertySchema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
17
|
-
type: z.ZodOptional<z.ZodEnum<{
|
|
18
|
-
string: "string";
|
|
19
|
-
number: "number";
|
|
20
|
-
boolean: "boolean";
|
|
21
|
-
integer: "integer";
|
|
22
|
-
}>>;
|
|
23
|
-
description: z.ZodOptional<z.ZodString>;
|
|
24
|
-
default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
25
|
-
enum: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
26
|
-
}, z.core.$strip>>>;
|
|
27
|
-
}, z.core.$strip>;
|
|
28
|
-
export type PropertySchema = z.infer<typeof PropertySchemaSchema>;
|
|
29
|
-
export type SpawnConfig = z.infer<typeof SpawnConfigSchema>;
|
|
30
|
-
export type GlobalsType = Record<string, unknown>;
|
|
31
|
-
export type PropertySchemaType = Record<string, PropertySchema>;
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
export const PropertySchemaSchema = z.object({
|
|
3
|
-
type: z.enum(['string', 'number', 'integer', 'boolean']).optional(),
|
|
4
|
-
description: z.string().optional(),
|
|
5
|
-
default: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
|
6
|
-
enum: z.array(z.string()).optional(),
|
|
7
|
-
});
|
|
8
|
-
export const SpawnConfigSchema = z.object({
|
|
9
|
-
inherits: z.string().optional(),
|
|
10
|
-
globals: z.record(z.string(), z.unknown()).optional(),
|
|
11
|
-
propertySchema: z.record(z.string(), PropertySchemaSchema).optional(),
|
|
12
|
-
});
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { SpawnResolver } from './SpawnResolver.js';
|
|
2
|
-
export default class SpawnEngine {
|
|
3
|
-
private engine;
|
|
4
|
-
private spawnResolver;
|
|
5
|
-
constructor(resolver: SpawnResolver);
|
|
6
|
-
/**
|
|
7
|
-
* Renders the specified Spawn
|
|
8
|
-
* A Spawn is a directory that contains templates, an optional spawn.json file, and template parameters in folder and filenames
|
|
9
|
-
*
|
|
10
|
-
* This is done by performing the following
|
|
11
|
-
*
|
|
12
|
-
* - Will recursively walk the spawn copying any files or directories encountered
|
|
13
|
-
* -- If a template file is encountered, it will be parsed and rendered prior to copying
|
|
14
|
-
*
|
|
15
|
-
* @param spawn the name of the spawn to parse and render. This is the name of the directory containing the spawn.json
|
|
16
|
-
* @param destination the target directory where rendered data will be sent
|
|
17
|
-
* @param context the values to be provided to the templates while rendering
|
|
18
|
-
* @return a promise containing all the original values plus any added during rendering
|
|
19
|
-
*/
|
|
20
|
-
renderSpawn(spawn: string, destination: string, context?: Record<string, unknown>): Promise<Record<string, unknown> | undefined>;
|
|
21
|
-
private promptForMissingProperties;
|
|
22
|
-
private _renderFile;
|
|
23
|
-
private _renderDirectory;
|
|
24
|
-
private logDebug;
|
|
25
|
-
private shouldIgnore;
|
|
26
|
-
}
|
|
27
|
-
export declare const spawnEngine: SpawnEngine;
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import fsP from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { Liquid } from 'liquidjs';
|
|
5
|
-
import { confirm, input, number, select } from '@inquirer/prompts';
|
|
6
|
-
import { SpawnConfigSchema } from './SpawnConfig.js';
|
|
7
|
-
import { spawnResolver } from './SpawnResolver.js';
|
|
8
|
-
function upperFirst(s) {
|
|
9
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
10
|
-
}
|
|
11
|
-
function camelCase(s) {
|
|
12
|
-
return s
|
|
13
|
-
.replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase())
|
|
14
|
-
.replace(/^(.)/, (_, c) => c.toLowerCase());
|
|
15
|
-
}
|
|
16
|
-
export default class SpawnEngine {
|
|
17
|
-
engine;
|
|
18
|
-
spawnResolver;
|
|
19
|
-
constructor(resolver) {
|
|
20
|
-
this.spawnResolver = resolver;
|
|
21
|
-
this.engine = new Liquid({ cache: true });
|
|
22
|
-
this.engine.registerFilter('packageToPath', (v) => v.replaceAll('.', '/'));
|
|
23
|
-
this.engine.registerFilter('encodePackage', (v) => {
|
|
24
|
-
v = v.replaceAll('-', '_');
|
|
25
|
-
v = v.replace(/\.(\d+)/g, '._$1');
|
|
26
|
-
return v;
|
|
27
|
-
});
|
|
28
|
-
this.engine.registerFilter('camelCase', (v) => camelCase(v));
|
|
29
|
-
this.engine.registerFilter('upperFirst', (v) => upperFirst(v));
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Renders the specified Spawn
|
|
33
|
-
* A Spawn is a directory that contains templates, an optional spawn.json file, and template parameters in folder and filenames
|
|
34
|
-
*
|
|
35
|
-
* This is done by performing the following
|
|
36
|
-
*
|
|
37
|
-
* - Will recursively walk the spawn copying any files or directories encountered
|
|
38
|
-
* -- If a template file is encountered, it will be parsed and rendered prior to copying
|
|
39
|
-
*
|
|
40
|
-
* @param spawn the name of the spawn to parse and render. This is the name of the directory containing the spawn.json
|
|
41
|
-
* @param destination the target directory where rendered data will be sent
|
|
42
|
-
* @param context the values to be provided to the templates while rendering
|
|
43
|
-
* @return a promise containing all the original values plus any added during rendering
|
|
44
|
-
*/
|
|
45
|
-
async renderSpawn(spawn, destination, context) {
|
|
46
|
-
let contextInternal;
|
|
47
|
-
if (!fs.existsSync(destination)) {
|
|
48
|
-
const source = await this.spawnResolver.resolveSpawn(spawn);
|
|
49
|
-
const sources = [source];
|
|
50
|
-
const currentConfigPath = path.resolve(source, 'spawn.json');
|
|
51
|
-
if (fs.existsSync(currentConfigPath)) {
|
|
52
|
-
const spawns = [];
|
|
53
|
-
let currentConfig = currentConfigPath;
|
|
54
|
-
let currentSpawn = SpawnConfigSchema.parse(JSON.parse(fs.readFileSync(currentConfig, { encoding: 'utf8' })));
|
|
55
|
-
spawns.push(currentSpawn);
|
|
56
|
-
// follow inheritance and build stack
|
|
57
|
-
while (currentSpawn.inherits) {
|
|
58
|
-
const inheritDir = path.resolve(path.dirname(currentConfig), currentSpawn.inherits);
|
|
59
|
-
currentConfig = path.resolve(inheritDir, 'spawn.json');
|
|
60
|
-
this.logDebug(`Inheriting from ${currentConfig}\n`);
|
|
61
|
-
if (!fs.existsSync(currentConfig)) {
|
|
62
|
-
throw new Error(`Inherited spawn ${currentConfig} does not exist`);
|
|
63
|
-
}
|
|
64
|
-
currentSpawn = SpawnConfigSchema.parse(JSON.parse(fs.readFileSync(currentConfig, { encoding: 'utf8' })));
|
|
65
|
-
spawns.push(currentSpawn);
|
|
66
|
-
sources.push(inheritDir);
|
|
67
|
-
}
|
|
68
|
-
let globals = {};
|
|
69
|
-
let propertySchemas = {};
|
|
70
|
-
for (const spawnConfig of spawns.reverse()) {
|
|
71
|
-
if (spawnConfig.globals) {
|
|
72
|
-
globals = { ...globals, ...spawnConfig.globals };
|
|
73
|
-
}
|
|
74
|
-
if (spawnConfig.propertySchema) {
|
|
75
|
-
propertySchemas = { ...propertySchemas, ...spawnConfig.propertySchema };
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
contextInternal = { ...globals, ...context };
|
|
79
|
-
contextInternal = await this.promptForMissingProperties(propertySchemas, contextInternal);
|
|
80
|
-
}
|
|
81
|
-
await fsP.mkdir(destination, { recursive: true });
|
|
82
|
-
for (const src of sources.reverse()) {
|
|
83
|
-
await this._renderDirectory(src, src, destination, contextInternal);
|
|
84
|
-
}
|
|
85
|
-
return contextInternal;
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
throw new Error(`The target directory ${destination} already exists`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
async promptForMissingProperties(propertySchema, context) {
|
|
92
|
-
const ret = context ? { ...context } : {};
|
|
93
|
-
let hasPrompted = false;
|
|
94
|
-
for (const key in propertySchema) {
|
|
95
|
-
if (!Object.prototype.hasOwnProperty.call(ret, key)) {
|
|
96
|
-
if (!hasPrompted) {
|
|
97
|
-
console.log('Please provide the following...\n');
|
|
98
|
-
hasPrompted = true;
|
|
99
|
-
}
|
|
100
|
-
const schema = propertySchema[key];
|
|
101
|
-
let message;
|
|
102
|
-
if (schema.description?.includes('{{')) {
|
|
103
|
-
message = this.engine.parseAndRenderSync(schema.description, ret);
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
message = schema.description ?? key;
|
|
107
|
-
}
|
|
108
|
-
let defaultValue;
|
|
109
|
-
if (typeof schema.default === 'string' && schema.default.includes('{{')) {
|
|
110
|
-
defaultValue = this.engine.parseAndRenderSync(schema.default, ret);
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
defaultValue = schema.default;
|
|
114
|
-
}
|
|
115
|
-
if (schema.type === 'boolean') {
|
|
116
|
-
ret[key] = await confirm({ message, default: typeof defaultValue === 'boolean' ? defaultValue : undefined });
|
|
117
|
-
}
|
|
118
|
-
else if (schema.enum) {
|
|
119
|
-
ret[key] = await select({
|
|
120
|
-
message,
|
|
121
|
-
choices: schema.enum.map((v) => ({ value: v })),
|
|
122
|
-
default: defaultValue !== undefined ? String(defaultValue) : undefined
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
else if (schema.type === 'number' || schema.type === 'integer') {
|
|
126
|
-
ret[key] = await number({ message, default: typeof defaultValue === 'number' ? defaultValue : undefined });
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
ret[key] = await input({ message, default: typeof defaultValue === 'string' ? defaultValue : undefined });
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return ret;
|
|
134
|
-
}
|
|
135
|
-
async _renderFile(source, destination, errorIfExists, context) {
|
|
136
|
-
if (destination.includes('{{')) {
|
|
137
|
-
destination = await this.engine.parseAndRender(destination, context ?? {});
|
|
138
|
-
}
|
|
139
|
-
let overwritingFile = false;
|
|
140
|
-
if ((errorIfExists) && fs.existsSync(destination)) {
|
|
141
|
-
if (errorIfExists) {
|
|
142
|
-
throw new Error(`The target file ${destination} already exists`);
|
|
143
|
-
}
|
|
144
|
-
overwritingFile = true;
|
|
145
|
-
}
|
|
146
|
-
let readStream;
|
|
147
|
-
if (destination.endsWith('.liquid')) {
|
|
148
|
-
destination = destination.substring(0, destination.length - 7);
|
|
149
|
-
readStream = await this.engine.renderFileToNodeStream(source, context ?? {});
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
readStream = fs.createReadStream(source);
|
|
153
|
-
}
|
|
154
|
-
this.logDebug(`${overwritingFile ? 'Overwriting' : 'Writing'} File\n${source}\nto\n${destination}\n`);
|
|
155
|
-
await fsP.mkdir(path.dirname(destination), { recursive: true });
|
|
156
|
-
const writeStream = fs.createWriteStream(destination);
|
|
157
|
-
readStream.pipe(writeStream);
|
|
158
|
-
}
|
|
159
|
-
async _renderDirectory(baseFrom, from, baseTo, context) {
|
|
160
|
-
const files = await fsP.readdir(from, { withFileTypes: true });
|
|
161
|
-
for (const file of files) {
|
|
162
|
-
const filePath = path.resolve(from, file.name);
|
|
163
|
-
const to = filePath.replace(baseFrom, baseTo);
|
|
164
|
-
if (file.isFile()) {
|
|
165
|
-
if (!this.shouldIgnore(file.name)) {
|
|
166
|
-
await this._renderFile(filePath, to, false, context);
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
this.logDebug(`Skipping File\n${filePath}\n`);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
else {
|
|
173
|
-
await this._renderDirectory(baseFrom, filePath, baseTo, context);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
logDebug(message) {
|
|
178
|
-
if (process.env.DEBUG) {
|
|
179
|
-
console.debug(message);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
shouldIgnore(fileName) {
|
|
183
|
-
const filesToSkip = ['.DS_Store', 'spawn.json'];
|
|
184
|
-
return filesToSkip.includes(fileName);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
export const spawnEngine = new SpawnEngine(spawnResolver);
|