@ptkl/toolkit 0.7.2 → 0.8.8

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.
@@ -2,7 +2,8 @@ import { Command } from "commander";
2
2
  import { build, createServer } from 'vite';
3
3
  import { c } from 'tar';
4
4
  import { Writable } from "stream";
5
- import { writeFileSync } from "fs";
5
+ import { writeFileSync, readFileSync } from "fs";
6
+ import axios from 'axios';
6
7
  import Util from "../lib/util.js";
7
8
  import { join } from 'path';
8
9
  class ForgeCommand {
@@ -26,7 +27,19 @@ class ForgeCommand {
26
27
  .action(this.runDev))
27
28
  .addCommand(new Command("list")
28
29
  .description("List all available apps")
29
- .action(this.listApps));
30
+ .action(this.listApps))
31
+ .addCommand(new Command("install")
32
+ .description("Dry-run the app install script locally against the active profile (no actual installation). Runs for both dev and live by default.")
33
+ .requiredOption("-p, --path <path>", "Path to the app directory (where ptkl.config.js is located)")
34
+ .option("--env <env>", "Run for a specific env only (dev or live). Omit to run for both.")
35
+ .option("-b, --bundle", "Build the bundle before running the install script")
36
+ .action((options) => this.install(options)))
37
+ .addCommand(new Command("uninstall")
38
+ .description("Dry-run the app uninstall script locally against the active profile (no actual uninstallation). Runs for both dev and live by default.")
39
+ .requiredOption("-p, --path <path>", "Path to the app directory (where ptkl.config.js is located)")
40
+ .option("--env <env>", "Run for a specific env only (dev or live). Omit to run for both.")
41
+ .option("-b, --bundle", "Build the bundle before running the uninstall script")
42
+ .action((options) => this.uninstall(options)));
30
43
  }
31
44
  async bundle(options) {
32
45
  const { path, upload } = options;
@@ -36,7 +49,13 @@ class ForgeCommand {
36
49
  // Change to the app directory
37
50
  process.chdir(path);
38
51
  const module = await import(`${path}/ptkl.config.js`);
39
- const { views, name, version, distPath, icon, type, label, permissions, } = module.default ?? {};
52
+ const { views, name, version, distPath, icon, type, label, permissions, install_permissions, runtime_permissions, entitlements, requires, scripts, ssrRenderer, } = module.default ?? {};
53
+ // Validate combined permissions limit
54
+ const rtPerms = runtime_permissions ?? [];
55
+ const ents = entitlements ?? [];
56
+ if (rtPerms.length + ents.length > 100) {
57
+ throw new Error(`Combined runtime_permissions (${rtPerms.length}) and entitlements (${ents.length}) must not exceed 100 entries`);
58
+ }
40
59
  // build manifest file
41
60
  const manifest = {
42
61
  name,
@@ -45,52 +64,212 @@ class ForgeCommand {
45
64
  label,
46
65
  icon,
47
66
  permissions,
67
+ install_permissions: install_permissions ?? [],
68
+ runtime_permissions: runtime_permissions ?? [],
69
+ entitlements: entitlements ?? [],
70
+ requires: requires ?? null,
71
+ scripts: {},
48
72
  type: type || 'platform', // default to 'platform' if not specified
73
+ ssrRenderer,
49
74
  };
50
75
  const client = Util.getClientForProfile();
51
76
  // get base url of the platform client
52
77
  const baseUrl = client.getPlatformBaseURL();
53
- const base = `${baseUrl}/luma/appservice/v1/forge/static/bundle/${name}/${version}/`;
54
- manifest.icon = `${base}/${icon}`;
55
- const buildViews = Object.keys(views).map((view) => {
56
- manifest.views[view] = `${view}.bundle.js`;
57
- return build({
78
+ // Different build approach for public vs platform apps
79
+ if (type === 'public') {
80
+ // Public apps: standard SPA build with index.html
81
+ manifest.icon = icon;
82
+ console.log("Building public app...");
83
+ await build({
58
84
  root: path,
59
- base,
60
85
  build: {
61
- rollupOptions: {
62
- input: views[view],
63
- output: {
64
- format: 'esm',
65
- entryFileNames: `[name].bundle.js`,
66
- assetFileNames: (assetInfo) => {
67
- return '[name].[ext]'; // Example: Customize the output file name format
86
+ outDir: distPath,
87
+ emptyOutDir: true,
88
+ }
89
+ });
90
+ console.log("✅ Public app build completed successfully");
91
+ // Build SSR renderer if specified for public apps
92
+ if (ssrRenderer) {
93
+ try {
94
+ console.log('Building SSR renderer...');
95
+ await build({
96
+ root: path,
97
+ build: {
98
+ outDir: distPath,
99
+ emptyOutDir: false,
100
+ target: 'esnext',
101
+ ssr: ssrRenderer, // Mark this as an SSR build with the entry point
102
+ rollupOptions: {
103
+ output: {
104
+ format: 'esm',
105
+ entryFileNames: 'ssr-renderer.js',
106
+ inlineDynamicImports: true,
107
+ },
108
+ }
109
+ },
110
+ ssr: {
111
+ noExternal: true, // Bundle all dependencies
112
+ target: 'webworker', // Browser-compatible build
113
+ },
114
+ resolve: {
115
+ conditions: ['browser'], // Use browser versions of packages
116
+ },
117
+ });
118
+ manifest.ssrRenderer = 'ssr-renderer.js';
119
+ console.log('✓ SSR renderer built successfully');
120
+ }
121
+ catch (error) {
122
+ console.error('✗ Failed to build SSR renderer:', error.message || error);
123
+ throw new Error('SSR renderer build failed.');
124
+ }
125
+ }
126
+ console.log("✅ All builds completed successfully");
127
+ }
128
+ else {
129
+ // Platform apps: custom bundle format with views and scripts
130
+ const base = `${baseUrl}/luma/appservice/v1/forge/static/bundle/${name}/${version}/`;
131
+ manifest.icon = `${base}${icon}`;
132
+ const buildViews = Object.keys(views).map(async (view) => {
133
+ manifest.views[view] = `${view}.bundle.js`;
134
+ try {
135
+ const result = await build({
136
+ root: path,
137
+ base,
138
+ build: {
139
+ outDir: distPath,
140
+ emptyOutDir: false,
141
+ rollupOptions: {
142
+ input: views[view],
143
+ output: {
144
+ format: 'esm',
145
+ entryFileNames: `[name].bundle.js`,
146
+ assetFileNames: (assetInfo) => {
147
+ return '[name].[ext]'; // Example: Customize the output file name format
148
+ },
149
+ manualChunks: undefined,
150
+ inlineDynamicImports: true,
151
+ }
68
152
  },
69
- manualChunks: undefined,
70
- inlineDynamicImports: true,
71
- }
72
- },
73
- },
74
- plugins: [
75
- {
76
- name: 'transform-dynamic-imports',
77
- generateBundle(options, bundle) {
78
- // Transform after bundling is complete
79
- for (const fileName in bundle) {
80
- const chunk = bundle[fileName];
81
- if (chunk.type === 'chunk' && chunk.code) {
82
- // Transform dynamic imports in the final bundled code
83
- chunk.code = chunk.code.replace(/import\(['"`]\.\/([^'"`]+)['"`]\)/g, `dynamicImport('${base}$1')`);
84
- // Also handle relative paths without ./
85
- chunk.code = chunk.code.replace(/import\(['"`]([^'"`\/]+\.js)['"`]\)/g, `dynamicImport('${base}$1')`);
153
+ },
154
+ plugins: [
155
+ {
156
+ name: 'transform-dynamic-imports',
157
+ generateBundle(options, bundle) {
158
+ // Transform after bundling is complete
159
+ for (const fileName in bundle) {
160
+ const chunk = bundle[fileName];
161
+ if (chunk.type === 'chunk' && chunk.code) {
162
+ // Transform dynamic imports in the final bundled code
163
+ chunk.code = chunk.code.replace(/import\(['"`]\.\/([^'"`]+)['"`]\)/g, `dynamicImport('${base}$1')`);
164
+ // Also handle relative paths without ./
165
+ chunk.code = chunk.code.replace(/import\(['"`]([^'"`\/]+\.js)['"`]\)/g, `dynamicImport('${base}$1')`);
166
+ }
167
+ }
168
+ }
169
+ },
170
+ ]
171
+ });
172
+ return result;
173
+ }
174
+ catch (error) {
175
+ throw error;
176
+ }
177
+ });
178
+ const buildScripts = scripts ? Object.keys(scripts).map(async (script) => {
179
+ manifest.scripts[script] = `${script}.script.js`;
180
+ try {
181
+ const result = await build({
182
+ root: path,
183
+ base,
184
+ build: {
185
+ outDir: distPath,
186
+ emptyOutDir: false,
187
+ target: 'esnext',
188
+ rollupOptions: {
189
+ input: scripts[script],
190
+ external: ['axios'],
191
+ output: {
192
+ format: 'esm',
193
+ entryFileNames: `[name].script.js`,
194
+ assetFileNames: (assetInfo) => {
195
+ return '[name].[ext]';
196
+ },
197
+ manualChunks: undefined,
198
+ inlineDynamicImports: true,
199
+ }
200
+ },
201
+ },
202
+ plugins: [
203
+ {
204
+ name: 'wrap-script-in-function',
205
+ generateBundle(options, bundle) {
206
+ for (const fileName in bundle) {
207
+ const chunk = bundle[fileName];
208
+ if (chunk.type === 'chunk' && chunk.code) {
209
+ let code = chunk.code;
210
+ // Replace `import X from 'axios'` → `const X = axiosAdapter`
211
+ // ([\w$]+ handles both regular names and $ from minification)
212
+ // \s* instead of \s+ because minifier removes space before the quote
213
+ code = code.replace(/import\s+([\w$]+)\s+from\s*['"]axios['"]\s*;?\n?/g, 'const $1=axiosAdapter;\n');
214
+ // Replace `import { foo, bar as baz } from 'axios'`
215
+ code = code.replace(/import\s*\{([^}]+)\}\s*from\s*['"]axios['"]\s*;?\n?/g, (_, imports) => imports.split(',').map((part) => {
216
+ const [orig, alias] = part.trim().split(/\s+as\s+/).map((s) => s.trim());
217
+ return `const ${alias || orig}=axiosAdapter${orig === 'default' || !alias ? '' : `.${orig}`};\n`;
218
+ }).join(''));
219
+ // Wrap in async function so sandbox (isolate-vm / new Function) gets a callable back
220
+ chunk.code = `return async()=>{try{${code}}catch(err){const errorObj={_error_:true,message:err.message,name:err.name||'Error',stack:err.stack};Object.keys(err).forEach(key=>{errorObj[key]=err[key]});if(err.data)errorObj.data=err.data;if(err.statusCode)errorObj.statusCode=err.statusCode;if(err.response)errorObj.response=err.response;if(err.code)errorObj.code=err.code;return errorObj}}`;
221
+ }
222
+ }
86
223
  }
87
224
  }
225
+ ]
226
+ });
227
+ return result;
228
+ }
229
+ catch (error) {
230
+ throw error;
231
+ }
232
+ }) : [];
233
+ const viewResults = await Promise.allSettled(buildViews);
234
+ const failedViews = viewResults.filter(r => r.status === 'rejected');
235
+ if (failedViews.length > 0) {
236
+ console.error('\n❌ Failed to build views:');
237
+ failedViews.forEach((result, index) => {
238
+ const viewNames = Object.keys(views);
239
+ const failedIndices = viewResults.map((r, i) => r.status === 'rejected' ? i : -1).filter(i => i >= 0);
240
+ const viewName = viewNames[failedIndices[index]];
241
+ const reason = result.reason;
242
+ console.error(`\n View: ${viewName}`);
243
+ console.error(` Input: ${views[viewName]}`);
244
+ console.error(` Error: ${reason?.message || String(reason)}`);
245
+ if (reason?.stack) {
246
+ console.error(`\n${reason.stack}`);
247
+ }
248
+ });
249
+ throw new Error('View build failed. See errors above.');
250
+ }
251
+ if (scripts && Object.keys(scripts).length > 0) {
252
+ const scriptResults = await Promise.allSettled(buildScripts);
253
+ const failedScripts = scriptResults.filter(r => r.status === 'rejected');
254
+ if (failedScripts.length > 0) {
255
+ console.error('\n❌ Failed to build scripts:');
256
+ failedScripts.forEach((result, index) => {
257
+ const scriptNames = Object.keys(scripts);
258
+ const failedIndices = scriptResults.map((r, i) => r.status === 'rejected' ? i : -1).filter(i => i >= 0);
259
+ const scriptName = scriptNames[failedIndices[index]];
260
+ const reason = result.reason;
261
+ console.error(`\n Script: ${scriptName}`);
262
+ console.error(` Input: ${scripts[scriptName]}`);
263
+ console.error(` Error: ${reason?.message || String(reason)}`);
264
+ if (reason?.stack) {
265
+ console.error(`\n${reason.stack}`);
88
266
  }
89
- },
90
- ]
91
- });
92
- });
93
- await Promise.allSettled(buildViews);
267
+ });
268
+ throw new Error('Script build failed. See errors above.');
269
+ }
270
+ }
271
+ console.log("✅ All builds completed successfully");
272
+ }
94
273
  console.log("Packaging app...");
95
274
  // // write manifest file
96
275
  const manifestPath = `${distPath}/manifest.json`;
@@ -106,6 +285,8 @@ class ForgeCommand {
106
285
  c({ gzip: true, cwd: distPath }, ['.']).pipe(bufferStream).on('finish', () => {
107
286
  client.forge().bundleUpload(buffer).then(() => {
108
287
  console.log('Bundle uploaded successfully');
288
+ }).catch((error) => {
289
+ console.error('\x1b[31m%s\x1b[0m', `Bundle upload failed: ${error.response?.data?.message || error.message}`);
109
290
  });
110
291
  });
111
292
  }
@@ -154,5 +335,70 @@ class ForgeCommand {
154
335
  const { data } = await forge.list();
155
336
  console.log(data);
156
337
  }
338
+ async install(options) {
339
+ const { path, env, bundle } = options;
340
+ if (bundle)
341
+ await this.bundle({ path, upload: false });
342
+ const envs = env ? [env] : ['dev', 'live'];
343
+ for (const e of envs) {
344
+ await ForgeCommand._runScript(path, 'install', e);
345
+ }
346
+ }
347
+ async uninstall(options) {
348
+ const { path, env, bundle } = options;
349
+ if (bundle)
350
+ await this.bundle({ path, upload: false });
351
+ const envs = env ? [env] : ['dev', 'live'];
352
+ for (const e of envs) {
353
+ await ForgeCommand._runScript(path, 'uninstall', e);
354
+ }
355
+ }
356
+ static async _runScript(appPath, scriptType, env = 'dev') {
357
+ const module = await import(`${appPath}/ptkl.config.js`);
358
+ const { name, distPath, scripts } = module.default ?? {};
359
+ if (!name)
360
+ throw new Error(`Could not read app name from ${appPath}/ptkl.config.js`);
361
+ if (!scripts?.[scriptType]) {
362
+ console.log(`No '${scriptType}' script defined in ptkl.config.js`);
363
+ return;
364
+ }
365
+ const scriptFile = join(distPath, `${scriptType}.script.js`);
366
+ let scriptCode;
367
+ try {
368
+ scriptCode = readFileSync(scriptFile, 'utf-8');
369
+ }
370
+ catch {
371
+ throw new Error(`Script file not found: ${scriptFile}\nRun 'forge bundle' first.`);
372
+ }
373
+ const profile = Util.getCurrentProfile();
374
+ global.window = {
375
+ __ENV_VARIABLES__: {
376
+ API_HOST: profile.host,
377
+ INTEGRATION_API: `${profile.host}/luma/integrations`,
378
+ PROJECT_API_TOKEN: profile.token,
379
+ PROJECT_ENV: env,
380
+ }
381
+ };
382
+ global.axiosAdapter = axios;
383
+ console.log(`Running ${scriptType} script for '${name}' (profile: ${profile.name}, env: ${env})...\n`);
384
+ // The bundle is: `return async()=>{try{ ...code... }catch(err){return {_error_:true,...}}}`
385
+ // new Function(scriptCode) creates a function whose body is that code.
386
+ // Calling it returns the async function, which we then await.
387
+ const scriptFn = new Function(scriptCode)();
388
+ const result = await scriptFn();
389
+ // Cleanup globals
390
+ delete global.window;
391
+ delete global.axiosAdapter;
392
+ if (result && result._error_) {
393
+ console.error(`❌ Script error: ${result.message}`);
394
+ if (result.stack)
395
+ console.error(result.stack);
396
+ process.exit(1);
397
+ }
398
+ console.log(`✅ ${scriptType} script completed`);
399
+ if (result !== undefined && result !== null) {
400
+ console.log('Result:', JSON.stringify(result, null, 2));
401
+ }
402
+ }
157
403
  }
158
404
  export default new ForgeCommand();
@@ -1,6 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import util from "../lib/util.js";
3
- import Api from "@ptkl/sdk";
3
+ import { Platform as Api } from "@ptkl/sdk";
4
4
  import OutputFormatCommand from "./outputCommand.js";
5
5
  class FunctionsCommand {
6
6
  register() {
@@ -0,0 +1,38 @@
1
+ import { Command } from "commander";
2
+ import util from "../lib/util.js";
3
+ import { Platform as Api } from "@ptkl/sdk/beta";
4
+ import { writeFileSync, mkdirSync } from "node:fs";
5
+ import { dirname, resolve } from "node:path";
6
+ import { idlToModuleAugmentation } from "../lib/idlToDts.js";
7
+ class GenerateTypesCommand {
8
+ register() {
9
+ return new Command('generate-types')
10
+ .description('Fetch IDL definitions from the platform and emit a TypeScript ' +
11
+ 'module-augmentation .d.ts file that gives your project typed ' +
12
+ 'component models, component functions, and platform functions.')
13
+ .option('--output <path>', 'Path where the .d.ts file will be written', './types/ptkl.d.ts')
14
+ .option('--env <env>', 'Environment to fetch IDL from (uses current profile default when omitted)')
15
+ .action(this.generate);
16
+ }
17
+ async generate(options) {
18
+ const profile = util.getCurrentProfile();
19
+ const client = new Api({ token: profile.token, host: profile.host, env: options.env });
20
+ console.log(`⏳ Fetching IDL…`);
21
+ const { data } = await client.system().idl();
22
+ console.log(`✅ IDL fetched: ${Object.keys(data.components ?? {}).length} component(s), ${Object.keys(data.functions ?? {}).length} platform function(s)`);
23
+ const dts = idlToModuleAugmentation(data);
24
+ const outputPath = resolve(process.cwd(), options.output);
25
+ mkdirSync(dirname(outputPath), { recursive: true });
26
+ writeFileSync(outputPath, dts, 'utf8');
27
+ const componentCount = Object.keys(data.components ?? {}).length;
28
+ const functionCount = Object.keys(data.functions ?? {}).length;
29
+ console.log(`✅ Types generated: ${outputPath}\n` +
30
+ ` ${componentCount} component(s) • ${functionCount} platform function(s)`);
31
+ console.log('');
32
+ console.log('Next steps:');
33
+ console.log(' 1. Include the file in your tsconfig.json "include" array, or');
34
+ console.log(` add a triple-slash reference: /// <reference path="${options.output}" />`);
35
+ console.log(' 2. Enjoy typed component models and functions!');
36
+ }
37
+ }
38
+ export default new GenerateTypesCommand();
@@ -8,6 +8,8 @@ import apps from "./apps.js";
8
8
  import role from "./role.js";
9
9
  import forge from "./forge.js";
10
10
  import component from "./component.js";
11
+ import generateTypes from "./generate-types.js";
12
+ import validateIDL from "./validate-idl.js";
11
13
  export const commands = [
12
14
  profile.register(),
13
15
  users.register(),
@@ -16,6 +18,8 @@ export const commands = [
16
18
  role.register(),
17
19
  forge.register(),
18
20
  component.register(),
21
+ generateTypes.register(),
22
+ validateIDL.register(),
19
23
  new Command('init')
20
24
  .description("Init protokol toolkit")
21
25
  .action(Util.init)
@@ -1,7 +1,7 @@
1
1
  import { Command } from "commander";
2
2
  import util from "../lib/util.js";
3
3
  import cli from "../lib/cli.js";
4
- import { APIUser, User } from "@ptkl/sdk";
4
+ import { APIUser, Users as User } from "@ptkl/sdk";
5
5
  import password from '@inquirer/password';
6
6
  class ProfileCommand {
7
7
  register() {
@@ -1,6 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import util from "../lib/util.js";
3
- import Api from "@ptkl/sdk";
3
+ import { Platform as Api } from "@ptkl/sdk";
4
4
  import OutputFormatCommand from "./outputCommand.js";
5
5
  class RoleCommand {
6
6
  register() {
@@ -1,6 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import util from "../lib/util.js";
3
- import Api from "@ptkl/sdk";
3
+ import { Platform as Api } from "@ptkl/sdk";
4
4
  class ApiUsersCommand {
5
5
  register() {
6
6
  return new Command("users")
@@ -0,0 +1,115 @@
1
+ import { Command } from "commander";
2
+ import { readFileSync, existsSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import util from "../lib/util.js";
5
+ import { Platform as Api } from "@ptkl/sdk/beta";
6
+ class ValidateIDLCommand {
7
+ register() {
8
+ return new Command('validate-idl')
9
+ .description('Validate a value against a component, extension, or platform-function IDL. ' +
10
+ 'Pass either --ref to identify the target by ref, or --idl-file to supply an ' +
11
+ 'inline IDL. The value to validate can be given as a JSON string (--value) or ' +
12
+ 'read from a file (--value-file).')
13
+ .option('--ref <ref>', 'Compound ref identifying the IDL target.\n' +
14
+ ' component:ns::name — base component, default schema\n' +
15
+ ' component:ns::name.schema — base component, named schema\n' +
16
+ ' extension:ns::name/extName — extension IDL on a component\n' +
17
+ ' pfn:functionName — platform function input')
18
+ .option('--idl-file <path>', 'Path to a JSON file containing an inline EntityIDL object. Mutually exclusive with --ref.')
19
+ .option('--field <field>', 'Field key to validate against (required for component/extension refs; ignored for pfn).')
20
+ .option('--value <json>', 'JSON-encoded value to validate. Mutually exclusive with --value-file.')
21
+ .option('--value-file <path>', 'Path to a JSON file whose contents are the value to validate. Mutually exclusive with --value.')
22
+ .option('--env <env>', 'Environment to run validation in (uses current profile default when omitted).')
23
+ .action(this.run);
24
+ }
25
+ async run(options) {
26
+ // ── mutual-exclusion guards ────────────────────────────────────────────
27
+ if (options.ref && options.idlFile) {
28
+ console.error('❌ Provide either --ref or --idl-file, not both.');
29
+ process.exit(1);
30
+ }
31
+ if (!options.ref && !options.idlFile) {
32
+ console.error('❌ Provide either --ref or --idl-file.');
33
+ process.exit(1);
34
+ }
35
+ if (options.value !== undefined && options.valueFile) {
36
+ console.error('❌ Provide either --value or --value-file, not both.');
37
+ process.exit(1);
38
+ }
39
+ if (options.value === undefined && !options.valueFile) {
40
+ console.error('❌ Provide --value or --value-file.');
41
+ process.exit(1);
42
+ }
43
+ // ── resolve value ──────────────────────────────────────────────────────
44
+ let rawValue;
45
+ if (options.valueFile) {
46
+ const path = resolve(process.cwd(), options.valueFile);
47
+ if (!existsSync(path)) {
48
+ console.error(`❌ Value file not found: ${path}`);
49
+ process.exit(1);
50
+ }
51
+ rawValue = readFileSync(path, 'utf8');
52
+ }
53
+ else {
54
+ rawValue = options.value;
55
+ }
56
+ let value;
57
+ try {
58
+ value = JSON.parse(rawValue);
59
+ }
60
+ catch {
61
+ console.error('❌ Could not parse value as JSON.');
62
+ process.exit(1);
63
+ }
64
+ // ── resolve inline IDL ─────────────────────────────────────────────────
65
+ let inlineIDL;
66
+ if (options.idlFile) {
67
+ const path = resolve(process.cwd(), options.idlFile);
68
+ if (!existsSync(path)) {
69
+ console.error(`❌ IDL file not found: ${path}`);
70
+ process.exit(1);
71
+ }
72
+ try {
73
+ inlineIDL = JSON.parse(readFileSync(path, 'utf8'));
74
+ }
75
+ catch {
76
+ console.error('❌ Could not parse IDL file as JSON.');
77
+ process.exit(1);
78
+ }
79
+ }
80
+ const profile = util.getCurrentProfile();
81
+ const client = new Api({ token: profile.token, host: profile.host, env: options.env });
82
+ const target = options.ref ? `ref: ${options.ref}` : `inline IDL (${options.idlFile})`;
83
+ console.log(`⏳ Validating value against ${target}…`);
84
+ let result;
85
+ try {
86
+ const req = { value };
87
+ if (options.ref)
88
+ req.ref = options.ref;
89
+ if (inlineIDL)
90
+ req.idl = inlineIDL;
91
+ if (options.field)
92
+ req.field = options.field;
93
+ const { data } = await client.system().idlValidate(req);
94
+ result = data;
95
+ }
96
+ catch (err) {
97
+ const msg = err?.response?.data?.message ?? err?.message ?? String(err);
98
+ console.error(`❌ Request failed: ${msg}`);
99
+ process.exit(1);
100
+ }
101
+ if (result.valid) {
102
+ console.log('✅ Valid');
103
+ return;
104
+ }
105
+ console.error('✗ Invalid');
106
+ if (result.errors?.length) {
107
+ for (const e of result.errors) {
108
+ const loc = e.field ? ` (${e.field})` : '';
109
+ console.error(` •${loc} ${e.message}`);
110
+ }
111
+ }
112
+ process.exit(1);
113
+ }
114
+ }
115
+ export default new ValidateIDLCommand();