@ptkl/toolkit 0.8.0 → 0.8.12

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.
@@ -14,12 +14,13 @@ program.hook('preAction', (thisCommand, actionCommand) => {
14
14
  });
15
15
  commands.forEach(c => program.addCommand(c));
16
16
  program.parseAsync(process.argv).catch((err) => {
17
+ console.log("An error occurred while executing the command.", err);
17
18
  const { response } = err;
18
19
  if (response && response.data) {
19
- console.error(response.data.message);
20
+ console.error(response.data.message || "An error occurred with the API request.");
20
21
  }
21
22
  else {
22
- console.error(err.message || err);
23
+ console.error(err.message || err || "An unknown error occurred.");
23
24
  }
24
25
  process.exit(1);
25
26
  });
@@ -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 ApiUsersCommand {
6
6
  register() {
@@ -1,6 +1,6 @@
1
1
  import { Command, Option } 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 { build, createServer } from 'vite';
5
5
  import { c } from 'tar';
6
6
  import { Writable } from "stream";
@@ -129,7 +129,7 @@ class AppsCommand {
129
129
  async bundle(options) {
130
130
  const { path, upload } = options;
131
131
  const module = await import(`${path}/ptkl.config.js`);
132
- const { views, name, version, distPath, icon, label, permissions, scripts, } = module.default ?? {};
132
+ const { views, name, version, distPath, icon, label, permissions, install_permissions, runtime_permissions, requires, scripts, } = module.default ?? {};
133
133
  // build manifest file
134
134
  const manifest = {
135
135
  name,
@@ -138,6 +138,9 @@ class AppsCommand {
138
138
  label,
139
139
  icon,
140
140
  permissions,
141
+ install_permissions: install_permissions ?? [],
142
+ runtime_permissions: runtime_permissions ?? [],
143
+ requires: requires ?? null,
141
144
  scripts,
142
145
  };
143
146
  const profile = Util.getCurrentProfile();
@@ -4,7 +4,7 @@ import { WebSocketServer } from "ws";
4
4
  import { resolve, join, dirname } from "path";
5
5
  import { rollup } from "rollup";
6
6
  import util from "../lib/util.js";
7
- import Api from "@ptkl/sdk";
7
+ import { Platform as Api } from "@ptkl/sdk";
8
8
  import { mkdirSync, writeFileSync, rmSync, readdirSync, existsSync, readFileSync } from "fs";
9
9
  import { nodeResolve } from '@rollup/plugin-node-resolve';
10
10
  // @ts-ignore
@@ -114,7 +114,7 @@ class ComponentCommand {
114
114
  // Merge user packages with internal dependencies
115
115
  const userPackages = componentConfig.packages || {};
116
116
  const dependencies = {
117
- '@ptkl/sdk': '^0.9.12',
117
+ '@ptkl/sdk': '^1.0.0',
118
118
  // Lit is bundled into the component, not provided by platform
119
119
  ...(framework === 'lit' ? {
120
120
  'lit': frameworkVersion ? `^${frameworkVersion}` : '^3.0.0'
@@ -301,16 +301,15 @@ class ComponentCommand {
301
301
  console.error(`❌ Build failed: ${error.message}`);
302
302
  socket.send(JSON.stringify({ error: error.message }));
303
303
  }
304
- // Temporary: Keep files for inspection
305
- // TODO: Uncomment cleanup when ready
306
- // finally {
307
- // try {
308
- // rmSync(tempDir, { recursive: true, force: true });
309
- // console.log(`Cleaned up temporary directory: ${tempDir}`);
310
- // } catch (cleanupError) {
311
- // console.error(`Failed to clean up temporary directory: ${cleanupError}`);
312
- // }
313
- // }
304
+ finally {
305
+ try {
306
+ rmSync(tempDir, { recursive: true, force: true });
307
+ console.log(`Cleaned up temporary directory: ${tempDir}`);
308
+ }
309
+ catch (cleanupError) {
310
+ console.error(`Failed to clean up temporary directory: ${cleanupError}`);
311
+ }
312
+ }
314
313
  }
315
314
  async buildLegacy(nodes, version, engine, socket) {
316
315
  try {
@@ -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, scripts, ssrRenderer, } = 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,6 +64,10 @@ 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,
48
71
  scripts: {},
49
72
  type: type || 'platform', // default to 'platform' if not specified
50
73
  ssrRenderer,
@@ -169,10 +192,7 @@ class ForgeCommand {
169
192
  format: 'esm',
170
193
  entryFileNames: `[name].script.js`,
171
194
  assetFileNames: (assetInfo) => {
172
- return '[name].[ext]'; // Example: Customize the output file name format
173
- },
174
- globals: {
175
- axios: 'axiosAdapter'
195
+ return '[name].[ext]';
176
196
  },
177
197
  manualChunks: undefined,
178
198
  inlineDynamicImports: true,
@@ -186,24 +206,17 @@ class ForgeCommand {
186
206
  for (const fileName in bundle) {
187
207
  const chunk = bundle[fileName];
188
208
  if (chunk.type === 'chunk' && chunk.code) {
189
- // Replace import statements with direct variable assignment from globals
190
- // Handle both regular and minified versions
191
- let code = chunk.code.replace(/import\s*(\w+)\s*from\s*['"]axios['"]\s*;?/g, 'const $1=axiosAdapter;');
192
- code = code.replace(/import\s*\{([^}]+)\}\s*from\s*['"]axios['"]\s*;?/g, (match, imports) => {
193
- // Handle named imports like { default as axios }
194
- const parts = imports.split(',').map(i => i.trim());
195
- return parts.map(part => {
196
- if (part.includes(' as ')) {
197
- const [original, alias] = part.split(' as ').map(s => s.trim());
198
- if (original === 'default') {
199
- return `const ${alias}=axiosAdapter;`;
200
- }
201
- return `const ${alias}=axiosAdapter.${original};`;
202
- }
203
- return `const ${part}=axiosAdapter.${part};`;
204
- }).join('');
205
- });
206
- // Wrap in async function (minified)
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
207
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}}`;
208
221
  }
209
222
  }
@@ -322,5 +335,70 @@ class ForgeCommand {
322
335
  const { data } = await forge.list();
323
336
  console.log(data);
324
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
+ }
325
403
  }
326
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();
@@ -0,0 +1,242 @@
1
+ /**
2
+ * IDL → TypeScript module-augmentation generator.
3
+ *
4
+ * Mirror of the runtime idlToDts.js used by the Monaco editor, but output
5
+ * targets a `declare module "@ptkl/sdk"` block so the generated `.d.ts` file
6
+ * extends the SDK's open interfaces at build time.
7
+ */
8
+ // ── Primitive type conversion ─────────────────────────────────────────────────
9
+ function idlTypeNodeToTs(node, indent = 0) {
10
+ if (!node)
11
+ return 'any';
12
+ switch (node.type) {
13
+ case 'string': return 'string';
14
+ case 'number': return 'number';
15
+ case 'boolean': return 'boolean';
16
+ case 'any': return 'any';
17
+ case 'array':
18
+ return node.items ? `Array<${idlTypeNodeToTs(node.items, indent)}>` : 'any[]';
19
+ case 'object':
20
+ if (node.fields && Object.keys(node.fields).length > 0) {
21
+ return objectShape(node.fields, indent);
22
+ }
23
+ return 'Record<string, any>';
24
+ case 'relation':
25
+ // When used as a standalone type (not as a field entry),
26
+ // a relation is a UUID string.
27
+ return 'string';
28
+ default:
29
+ // NamedRef or unknown — treat as any
30
+ return 'any';
31
+ }
32
+ }
33
+ const I = ' '; // indent unit
34
+ function objectShape(fields, indent = 0) {
35
+ const entries = Object.entries(fields);
36
+ // Keep short objects (≤3 simple fields) inline
37
+ if (entries.length <= 3 && entries.every(([, n]) => !n.fields && n.type !== 'object')) {
38
+ const props = entries.map(([key, node]) => {
39
+ const opt = node.required === false || node.required === undefined;
40
+ return `${key}${opt ? '?' : ''}: ${idlTypeNodeToTs(node, indent)}`;
41
+ });
42
+ return `{ ${props.join('; ')} }`;
43
+ }
44
+ const pad = I.repeat(indent + 1);
45
+ const closePad = I.repeat(indent);
46
+ const lines = entries.map(([key, node]) => {
47
+ const opt = node.required === false || node.required === undefined;
48
+ return `${pad}${key}${opt ? '?' : ''}: ${idlTypeNodeToTs(node, indent + 1)};`;
49
+ });
50
+ return `{\n${lines.join('\n')}\n${closePad}}`;
51
+ }
52
+ // ── Relation expansion ────────────────────────────────────────────────────────
53
+ /**
54
+ * Expands a field map into TypeScript property lines. Relation fields produce
55
+ * two entries: the UUID field (`field: string`) and the hydrated reference
56
+ * (`field_RefObj: T` or `field_RefObj: Array<T>`).
57
+ */
58
+ function expandFieldLines(fields, pad, indent) {
59
+ const lines = [];
60
+ for (const [k, node] of Object.entries(fields)) {
61
+ const opt = node.required === false || node.required === undefined;
62
+ if (node.type === 'relation') {
63
+ // UUID string field
64
+ lines.push(`${pad}${k}${opt ? '?' : ''}: string;`);
65
+ // Hydrated _RefObj field
66
+ const itemTs = node.items ? idlTypeNodeToTs(node.items, indent) : 'any';
67
+ const refType = node.cardinality === 'many' ? `Array<${itemTs}>` : itemTs;
68
+ lines.push(`${pad}${k}_RefObj?: ${refType};`);
69
+ }
70
+ else {
71
+ lines.push(`${pad}${k}${opt ? '?' : ''}: ${idlTypeNodeToTs(node, indent)};`);
72
+ }
73
+ }
74
+ return lines;
75
+ }
76
+ /**
77
+ * Like objectShape but expands relation fields into two properties.
78
+ */
79
+ function objectShapeWithRelations(fields, indent = 0) {
80
+ const entries = Object.entries(fields);
81
+ const hasRelation = entries.some(([, n]) => n.type === 'relation');
82
+ if (!hasRelation)
83
+ return objectShape(fields, indent); // fast path
84
+ const pad = I.repeat(indent + 1);
85
+ const closePad = I.repeat(indent);
86
+ const lines = expandFieldLines(fields, pad, indent + 1);
87
+ return `{\n${lines.join('\n')}\n${closePad}}`;
88
+ }
89
+ // ── Top-level generators ──────────────────────────────────────────────────────
90
+ function buildComponentModels(components, baseIndent) {
91
+ const entries = [];
92
+ const d = baseIndent;
93
+ const pad = I.repeat(d);
94
+ for (const [ref, comp] of Object.entries(components)) {
95
+ if (!comp)
96
+ continue;
97
+ // Collect all schema names from base + extensions.
98
+ const schemaNames = new Set();
99
+ for (const s of Object.keys(comp.schemas ?? {}))
100
+ schemaNames.add(s);
101
+ for (const ext of Object.values(comp.extensions ?? {})) {
102
+ if (!ext)
103
+ continue;
104
+ for (const s of Object.keys(ext.schemas ?? {}))
105
+ schemaNames.add(s);
106
+ }
107
+ for (const schema of schemaNames) {
108
+ const key = schema === 'default' ? ref : `${ref}.${schema}`;
109
+ const baseFields = comp.schemas?.[schema]?.fields;
110
+ // Collect extension entries for this schema.
111
+ const extEntries = [];
112
+ for (const [extName, ext] of Object.entries(comp.extensions ?? {})) {
113
+ if (!ext)
114
+ continue;
115
+ const extSchemaFields = ext.schemas?.[schema]?.fields;
116
+ if (extSchemaFields && Object.keys(extSchemaFields).length > 0) {
117
+ extEntries.push([extName, extSchemaFields]);
118
+ }
119
+ }
120
+ const hasBase = baseFields && Object.keys(baseFields).length > 0;
121
+ const hasExt = extEntries.length > 0;
122
+ if (!hasBase && !hasExt) {
123
+ entries.push(`${pad}${JSON.stringify(key)}: Record<string, any>;`);
124
+ continue;
125
+ }
126
+ const propLines = [];
127
+ if (hasBase) {
128
+ propLines.push(...expandFieldLines(baseFields, `${pad}${I}`, d + 1));
129
+ }
130
+ if (hasExt) {
131
+ const extInner = [];
132
+ for (const [extName, extFields] of extEntries) {
133
+ extInner.push(`${pad}${I}${I}${extName}: ${objectShapeWithRelations(extFields, d + 2)};`);
134
+ }
135
+ propLines.push(`${pad}${I}extensions: {\n${extInner.join('\n')}\n${pad}${I}};`);
136
+ }
137
+ entries.push(`${pad}${JSON.stringify(key)}: {\n${propLines.join('\n')}\n${pad}};`);
138
+ }
139
+ }
140
+ if (entries.length === 0)
141
+ return '';
142
+ return entries.join('\n');
143
+ }
144
+ function buildComponentFunctions(components, baseIndent) {
145
+ const entries = [];
146
+ const d = baseIndent;
147
+ const pad = I.repeat(d);
148
+ for (const [ref, comp] of Object.entries(components)) {
149
+ if (!comp)
150
+ continue;
151
+ const allFunctions = [
152
+ ...(comp.functions ?? []),
153
+ ...Object.values(comp.extensions ?? {}).flatMap(ext => ext?.functions ?? []),
154
+ ];
155
+ if (allFunctions.length === 0)
156
+ continue;
157
+ const fnLines = [];
158
+ for (const fn of allFunctions) {
159
+ const parts = [];
160
+ if (fn.input && Object.keys(fn.input).length > 0)
161
+ parts.push(`input: ${objectShape(fn.input, d + 2)}`);
162
+ if (fn.output)
163
+ parts.push(`output: ${idlTypeNodeToTs(fn.output, d + 2)}`);
164
+ if (parts.length === 0)
165
+ continue;
166
+ fnLines.push(`${pad}${I}${JSON.stringify(fn.name)}: { ${parts.join('; ')} };`);
167
+ }
168
+ if (fnLines.length > 0)
169
+ entries.push(`${pad}${JSON.stringify(ref)}: {\n${fnLines.join('\n')}\n${pad}};`);
170
+ }
171
+ if (entries.length === 0)
172
+ return '';
173
+ return entries.join('\n');
174
+ }
175
+ function buildPlatformFunctions(functions, baseIndent) {
176
+ const entries = [];
177
+ const d = baseIndent;
178
+ const pad = I.repeat(d);
179
+ for (const [name, sig] of Object.entries(functions)) {
180
+ if (!sig)
181
+ continue;
182
+ const parts = [];
183
+ if (sig.input && Object.keys(sig.input).length > 0)
184
+ parts.push(`input: ${objectShape(sig.input, d + 1)}`);
185
+ if (sig.output)
186
+ parts.push(`output: ${idlTypeNodeToTs(sig.output, d + 1)}`);
187
+ if (parts.length === 0)
188
+ continue;
189
+ entries.push(`${pad}${JSON.stringify(name)}: { ${parts.join('; ')} };`);
190
+ }
191
+ if (entries.length === 0)
192
+ return '';
193
+ return entries.join('\n');
194
+ }
195
+ // ── Main export ───────────────────────────────────────────────────────────────
196
+ /**
197
+ * Converts an IDL response into a `declare module "@ptkl/sdk"` augmentation
198
+ * block. Write the output to a `.d.ts` file and include it in your project
199
+ * (e.g., via `tsconfig.json` `"include"` or a triple-slash reference).
200
+ */
201
+ export function idlToModuleAugmentation(idl) {
202
+ const hasComponents = idl.components && Object.keys(idl.components).length > 0;
203
+ const hasFunctions = idl.functions && Object.keys(idl.functions).length > 0;
204
+ if (!hasComponents && !hasFunctions) {
205
+ return `// No IDL definitions found — nothing to generate.\n`;
206
+ }
207
+ const date = new Date().toISOString().slice(0, 10);
208
+ const lines = [
209
+ `// Generated by ptkl generate-types on ${date}`,
210
+ `// Do not edit manually — re-run \`ptkl generate-types\` to refresh.`,
211
+ ``,
212
+ `import "@ptkl/sdk";`,
213
+ `import "@ptkl/sdk/beta";`,
214
+ ];
215
+ // Declare interfaces once globally, then extend into both modules.
216
+ const ifaceNames = [];
217
+ if (hasComponents) {
218
+ const models = buildComponentModels(idl.components, 1);
219
+ if (models) {
220
+ lines.push(``, `interface _CM {\n${models}\n}`);
221
+ ifaceNames.push('ComponentModels extends _CM');
222
+ }
223
+ const fns = buildComponentFunctions(idl.components, 1);
224
+ if (fns) {
225
+ lines.push(``, `interface _CF {\n${fns}\n}`);
226
+ ifaceNames.push('ComponentFunctions extends _CF');
227
+ }
228
+ }
229
+ if (hasFunctions) {
230
+ const platFns = buildPlatformFunctions(idl.functions, 1);
231
+ if (platFns) {
232
+ lines.push(``, `interface _PF {\n${platFns}\n}`);
233
+ ifaceNames.push('PlatformFunctions extends _PF');
234
+ }
235
+ }
236
+ if (ifaceNames.length > 0) {
237
+ const augBody = ifaceNames.map(n => ` interface ${n} {}`).join('\n');
238
+ lines.push(``, `declare module "@ptkl/sdk" {\n${augBody}\n}`, ``, `declare module "@ptkl/sdk/beta" {\n${augBody}\n}`);
239
+ }
240
+ lines.push(``);
241
+ return lines.join('\n');
242
+ }
package/dist/lib/util.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
- import Api from '@ptkl/sdk';
2
+ import { Platform } from '@ptkl/sdk';
3
3
  export default class Util {
4
4
  static profilePath = `${process.env.HOME}/.ptkl`;
5
5
  static fileName = "profiles.json";
@@ -69,6 +69,6 @@ export default class Util {
69
69
  }
70
70
  static getClientForProfile() {
71
71
  const profile = Util.getCurrentProfile();
72
- return new Api({ project: profile.project, token: profile.token, host: profile.host });
72
+ return new Platform({ project: profile.project, token: profile.token, host: profile.host });
73
73
  }
74
74
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ptkl/toolkit",
3
- "version": "0.8.0",
3
+ "version": "0.8.12",
4
4
  "description": "A command-line toolkit for managing Protokol platform applications, profiles, functions, and components",
5
5
  "keywords": [
6
6
  "protokol",
@@ -35,7 +35,7 @@
35
35
  "@babel/preset-typescript": "^7.28.5",
36
36
  "@babel/standalone": "^7.26.10",
37
37
  "@inquirer/password": "^4.0.21",
38
- "@ptkl/sdk": "^0.9.12",
38
+ "@ptkl/sdk": "^1.5.1",
39
39
  "@rollup/plugin-babel": "^6.1.0",
40
40
  "@rollup/plugin-commonjs": "^29.0.0",
41
41
  "@rollup/plugin-node-resolve": "^16.0.3",
@@ -1,38 +0,0 @@
1
- import * as Babel from '@babel/standalone';
2
- import less from 'less';
3
- export default class Compiler {
4
- async compileBabel(expression) {
5
- let code = Babel.transform(expression, {
6
- sourceType: "module",
7
- presets: ["env"]
8
- });
9
- return code.code;
10
- }
11
- async compileCSS(scope, lang, expression) {
12
- switch (lang) {
13
- case 'less':
14
- if (scope) {
15
- return await less.render(`.${scope} { ${expression} }`);
16
- }
17
- const { css } = await less.render(`${expression}`);
18
- return css;
19
- default:
20
- return expression;
21
- }
22
- }
23
- async compile(ext, content) {
24
- switch (ext) {
25
- case 'js':
26
- return await this.compileBabel(content);
27
- case 'css':
28
- return await this.compileCSS(null, "css", content);
29
- case 'less':
30
- return await this.compileCSS(null, "less", content);
31
- default:
32
- return await Promise.resolve(content);
33
- }
34
- }
35
- getSupportedExt() {
36
- return ["js", "css", "less", "json", "svg", "html"];
37
- }
38
- }
@@ -1,96 +0,0 @@
1
- /**
2
- * Utility to generate import maps for component frameworks
3
- * This should be used on the frontend to dynamically create import maps
4
- * based on sdk_version and sdk_engine
5
- */
6
- /**
7
- * Generate an import map configuration for a given framework
8
- *
9
- * @param sdkEngine - The engine string (e.g., "react@18", "lit@3", "vue3")
10
- * @returns Import map configuration object
11
- *
12
- * @example
13
- * ```typescript
14
- * const importMap = generateImportMap("react@18");
15
- * // Inject into HTML:
16
- * const script = document.createElement('script');
17
- * script.type = 'importmap';
18
- * script.textContent = JSON.stringify(importMap);
19
- * document.head.appendChild(script);
20
- * ```
21
- */
22
- export function generateImportMap(sdkEngine) {
23
- // Parse engine to extract framework and version (e.g., "lit@3" -> { framework: "lit", version: "3" })
24
- const engineMatch = sdkEngine.match(/^([^@]+)(?:@(.+))?$/);
25
- const framework = engineMatch ? engineMatch[1] : sdkEngine;
26
- const frameworkVersion = engineMatch ? engineMatch[2] : null;
27
- const imports = {};
28
- if (framework === 'react') {
29
- const version = frameworkVersion || '18';
30
- imports['react'] = `https://esm.sh/react@${version}`;
31
- imports['react-dom'] = `https://esm.sh/react-dom@${version}`;
32
- imports['react/jsx-runtime'] = `https://esm.sh/react@${version}/jsx-runtime`;
33
- imports['react/jsx-dev-runtime'] = `https://esm.sh/react@${version}/jsx-dev-runtime`;
34
- }
35
- else if (framework === 'vue3' || framework === 'vue') {
36
- const version = frameworkVersion || '3';
37
- imports['vue'] = `https://esm.sh/vue@${version}`;
38
- }
39
- else if (framework === 'lit') {
40
- const version = frameworkVersion || '3';
41
- imports['lit'] = `https://esm.sh/lit@${version}`;
42
- imports['lit/decorators.js'] = `https://esm.sh/lit@${version}/decorators.js`;
43
- imports['lit/directive.js'] = `https://esm.sh/lit@${version}/directive.js`;
44
- imports['lit/directives/class-map.js'] = `https://esm.sh/lit@${version}/directives/class-map.js`;
45
- imports['lit/directives/style-map.js'] = `https://esm.sh/lit@${version}/directives/style-map.js`;
46
- imports['lit/directives/if-defined.js'] = `https://esm.sh/lit@${version}/directives/if-defined.js`;
47
- imports['lit/directives/repeat.js'] = `https://esm.sh/lit@${version}/directives/repeat.js`;
48
- imports['lit/directives/unsafe-html.js'] = `https://esm.sh/lit@${version}/directives/unsafe-html.js`;
49
- }
50
- else if (framework === 'webcomponents') {
51
- // For vanilla web components, no external framework needed
52
- return { imports: {} };
53
- }
54
- return { imports };
55
- }
56
- /**
57
- * Generate the import map HTML script tag as a string
58
- *
59
- * @param sdkEngine - The engine string (e.g., "react@18", "lit@3", "vue3")
60
- * @returns HTML string for the import map script tag
61
- *
62
- * @example
63
- * ```typescript
64
- * const scriptTag = generateImportMapHTML("react@18");
65
- * document.head.insertAdjacentHTML('beforeend', scriptTag);
66
- * ```
67
- */
68
- export function generateImportMapHTML(sdkEngine) {
69
- const importMap = generateImportMap(sdkEngine);
70
- if (Object.keys(importMap.imports).length === 0) {
71
- return ''; // No import map needed
72
- }
73
- return `<script type="importmap">\n${JSON.stringify(importMap, null, 2)}\n</script>`;
74
- }
75
- /**
76
- * Inject import map into the document dynamically
77
- *
78
- * @param sdkEngine - The engine string (e.g., "react@18", "lit@3", "vue3")
79
- *
80
- * @example
81
- * ```typescript
82
- * // In your frontend code:
83
- * injectImportMap("react@18");
84
- * // Now you can load component modules
85
- * ```
86
- */
87
- export function injectImportMap(sdkEngine) {
88
- const importMap = generateImportMap(sdkEngine);
89
- if (Object.keys(importMap.imports).length === 0) {
90
- return; // No import map needed
91
- }
92
- const script = document.createElement('script');
93
- script.type = 'importmap';
94
- script.textContent = JSON.stringify(importMap, null, 2);
95
- document.head.appendChild(script);
96
- }