@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 ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+
3
+ node "%~dp0\dev" %*
package/bin/dev.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env tsx
2
+ // eslint-disable-next-line node/shebang
3
+ (async () => {
4
+ const oclif = await import('@oclif/core')
5
+ await oclif.execute({type: 'esm', development: true, dir: import.meta.url})
6
+ })()
package/bin/run.cmd ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+
3
+ node "%~dp0\run" %*
package/bin/run.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ // eslint-disable-next-line node/shebang
3
+ (async () => {
4
+ const oclif = await import('@oclif/core')
5
+ await oclif.execute({type: 'esm', dir: import.meta.url})
6
+ })()
@@ -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/lib/interfaces").Arg<string, Record<string, unknown>>;
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 { spawnEngine } from '../../internal/spawn/SpawnEngine.js';
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 = { projectName: args.name };
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 = (await spawnEngine.renderSpawn('project', projectDir, context)) ?? 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 = path.resolve(name);
57
- if (!dir.startsWith(projectDir)) {
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/lib/interfaces").BooleanFlag<boolean>;
8
- force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
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/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
8
- entities: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
9
- repository: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
- mirror: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
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
  }
@@ -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/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
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/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
8
- publish: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
9
- verbose: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
- dryRun: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
11
- force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
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: it attaches the access token as a WebSocket upgrade
74
- // header. The factory is async so the token is refreshed before each (re)connect.
75
- connectionInfo.webSocketFactory = async () => {
76
- const token = await this.freshAccessToken(target.restBaseUrl);
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/login/device/refresh', {
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/login/device/start', {
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/login/device/token', {
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 }),
@@ -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;
@@ -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": "{{ kinoticApiVersion }}",
16
- "@kinotic-ai/persistence": "{{ kinoticApiVersion }}"
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
- "kinoticApiVersion": "^1.0.9"
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": "2.3.0",
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": "https://github.com/kinotic-ai/kinotic",
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.4.0",
24
+ "@kinotic-ai/core": "^3.1.0",
22
25
  "@kinotic-ai/idl": "^1.0.9",
23
- "@kinotic-ai/os-api": "^1.5.1",
24
- "@kinotic-ai/persistence": "^1.2.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.25.7",
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);