buckyos 0.7.160 → 0.7.162

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.
@@ -1,9 +1,14 @@
1
1
  import {
2
+ AIWS_MANIFEST_FILE,
3
+ AIWS_MANIFEST_VERSION,
2
4
  AiwsSourceError,
3
5
  buildControl,
4
6
  exportBundle,
7
+ isSourcePath,
5
8
  NfspClient,
9
+ packBuildFiles,
6
10
  pullControl,
11
+ templateDir,
7
12
  watchControl,
8
13
  } from 'buckyos/node'
9
14
  import type { CommandDefinition, CommandModule, JsonSchema, OptionDefinition, PositionalDefinition } from '../core/command.ts'
@@ -442,7 +447,30 @@ function list(input: Record<string, unknown>, key: string): string[] {
442
447
  return requiredInputString(input, key).split(',').map((s) => s.trim()).filter(Boolean)
443
448
  }
444
449
 
445
- // ---- local source folders: a developer's or an Agent's own disk (files go inline, ≤ 4 MiB per build)
450
+ const IDENT = /^did:[a-z0-9]+:[^/#\s]+#[a-z0-9][a-z0-9._-]{0,63}$/
451
+
452
+ /** A `--migration` file: `{ "<author_did>#<name>": Migration }` (aiworkspace schemas/migration.schema.json: `config` /
453
+ * `bindings` `{ rename, drop, fill }`, `slots` `{ rename, move }`, `events` `{ rename, drop }`), passed as it is. */
454
+ async function migrationFile(ctx: CommandContext, path: string): Promise<Record<string, unknown>> {
455
+ const host = getHost()
456
+ let value: unknown
457
+ try {
458
+ value = JSON.parse(await host.readTextFile(host.path.resolve(ctx.cwd, path)))
459
+ } catch (error) {
460
+ throw new UsageError('AIWS_USAGE', `--migration ${path}: ${error instanceof Error ? error.message : String(error)}`)
461
+ }
462
+ const shape = `--migration ${path}: { "<author_did>#<name>": { config?, bindings?, slots?, events? } }`
463
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new UsageError('AIWS_USAGE', shape)
464
+ for (const [ident, m] of Object.entries(value)) {
465
+ if (!IDENT.test(ident) || !m || typeof m !== 'object' || Array.isArray(m) || Object.keys(m).some((k) => !['config', 'bindings', 'slots', 'events'].includes(k))) {
466
+ throw new UsageError('AIWS_USAGE', `${shape} (at ${ident})`)
467
+ }
468
+ }
469
+ return value as Record<string, unknown>
470
+ }
471
+
472
+ // ---- local source folders: a developer's or an Agent's own disk (files go inline; past 4 MiB per build the largest
473
+ // are uploaded and sent by reference, Q22)
446
474
 
447
475
  const LOCAL_STATE = '.aiws-build.json'
448
476
 
@@ -450,23 +478,41 @@ async function isLocalDir(path: string): Promise<boolean> {
450
478
  try { return (await getHost().stat(path)).isDirectory } catch { return false }
451
479
  }
452
480
 
453
- /** The files of a local (flat) source folder as `ext.build` takes them inline. */
454
- async function localFiles(dir: string): Promise<{ name: string; base64: string }[]> {
481
+ /** The files of a local source folder, sub-folders included (names are relative paths; dot entries are skipped). */
482
+ async function localFiles(dir: string): Promise<{ name: string; bytes: Uint8Array }[]> {
455
483
  const host = getHost()
456
- const files: { name: string; base64: string }[] = []
484
+ const files: { name: string; bytes: Uint8Array }[] = []
457
485
  const problems: { file: string; severity: 'error'; message: string }[] = []
458
- let total = 0
459
- for (const entry of await host.readDir(dir)) {
460
- if (entry.name.startsWith('.')) continue
461
- if (!entry.isFile) { problems.push({ file: entry.name, severity: 'error', message: 'a control source folder is flat: move files out of sub-folders' }); continue }
462
- const bytes = await host.readFile(host.path.join(dir, entry.name))
463
- total += bytes.length
464
- files.push({ name: entry.name, base64: btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join('')) })
486
+ const walk = async (folder: string, prefix: string, depth: number): Promise<void> => {
487
+ for (const entry of await host.readDir(folder)) {
488
+ if (entry.name.startsWith('.')) continue
489
+ const name = `${prefix}${entry.name}`
490
+ if (entry.isDirectory) {
491
+ if (depth >= 8) problems.push({ file: name, severity: 'error', message: 'sub-folders nest at most 8 levels' })
492
+ else await walk(host.path.join(folder, entry.name), `${name}/`, depth + 1)
493
+ } else if (entry.isFile) files.push({ name, bytes: await host.readFile(host.path.join(folder, entry.name)) })
494
+ else problems.push({ file: name, severity: 'error', message: 'only files and folders belong to a control source' })
495
+ }
465
496
  }
497
+ await walk(dir, '', 0)
466
498
  if (files.length === 0) problems.push({ file: '', severity: 'error', message: `${dir} holds no files` })
467
- if (total > 4 * 1024 * 1024) problems.push({ file: '', severity: 'error', message: 'a control is limited to 4 MiB per build from a local folder' })
468
499
  if (problems.length > 0) throw new ToolError('BUILD_FAILED', `BUILD_FAILED: ${problems.map((p) => `${p.file}: ${p.message}`).join('; ')}`, undefined, false, { data: { diagnostics: problems } })
469
- return files.sort((a, b) => a.name.localeCompare(b.name))
500
+ return files.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
501
+ }
502
+
503
+ /** Put a source file too big to go inline into the caller's own `exports` bucket: the FileObject the build names (the
504
+ * service reads it through the NDM gateway with the caller's token). */
505
+ function sourceUploader(ctx: CommandContext): (name: string, bytes: Uint8Array) => Promise<string> {
506
+ return async (name, bytes) => {
507
+ const bucket = `user/aiworkspace/${ctx.principal.id}/exports`
508
+ const key = `aiws-src/${Date.now()}-${name.replaceAll('/', '_')}`
509
+ const url = `${gatewayOrigin(ctx.connection.endpoint)}/bucket/v1/${bucket}/${key.split('/').map(encodeURIComponent).join('/')}`
510
+ const response = await fetch(url, { method: 'PUT', headers: { Authorization: `Bearer ${await sessionToken(ctx)}` }, body: new Blob([bytes as Uint8Array<ArrayBuffer>]), signal: ctx.signal })
511
+ if (!response.ok) throw new ToolError('AIWS_UPLOAD_FAILED', `upload of ${name}: HTTP ${response.status} ${await response.text().catch(() => '')}`)
512
+ const put = await response.json() as { obj_id?: string }
513
+ if (!put.obj_id) throw new ToolError('AIWS_UPLOAD_FAILED', `upload of ${name}: the bucket answered no obj_id`)
514
+ return put.obj_id
515
+ }
470
516
  }
471
517
 
472
518
  async function localState(dir: string): Promise<Record<string, unknown> | null> {
@@ -474,13 +520,13 @@ async function localState(dir: string): Promise<Record<string, unknown> | null>
474
520
  }
475
521
 
476
522
  /** Build a local folder into a Workspace; the base it was built on is kept in its `.aiws-build.json`. */
477
- async function buildLocal(ctx: CommandContext, ws: string, dir: string, opts: { folderId?: string; force?: boolean; dryRun?: boolean }): Promise<Record<string, unknown>> {
523
+ async function buildLocal(ctx: CommandContext, ws: string, dir: string, opts: { folderId?: string; force?: boolean; dryRun?: boolean; migration?: Record<string, unknown> }): Promise<Record<string, unknown>> {
478
524
  const state = await localState(dir)
479
525
  const result = await aiws(ctx, 'ext.build', {
480
- workspace_id: ws, files: await localFiles(dir),
526
+ workspace_id: ws, files: await sourceErrors(async () => packBuildFiles(await localFiles(dir), sourceUploader(ctx))),
481
527
  ...(opts.folderId ? { folder_id: opts.folderId } : {}),
482
528
  ...(state && state.workspace_id === ws ? { expect: { def_rev: state.def_rev, dir_rev: state.dir_rev } } : {}),
483
- ...(opts.force ? { force: true } : {}), ...(opts.dryRun ? { dry_run: true } : {}),
529
+ ...(opts.force ? { force: true } : {}), ...(opts.dryRun ? { dry_run: true } : {}), ...(opts.migration ? { migration: opts.migration } : {}),
484
530
  })
485
531
  if (!opts.dryRun && typeof result.def_entity_id === 'string' && typeof result.def_rev === 'number') {
486
532
  await getHost().writeTextFile(getHost().path.join(dir, LOCAL_STATE), `${JSON.stringify({ workspace_id: ws, def_entity_id: result.def_entity_id, def_rev: result.def_rev, dir_rev: result.dir_rev ?? 0, ident: result.ident, at: new Date().toISOString() }, null, 2)}\n`)
@@ -491,7 +537,7 @@ async function buildLocal(ctx: CommandContext, ws: string, dir: string, opts: {
491
537
  const buildCommand: CommandDefinition = {
492
538
  verb: 'build',
493
539
  summary: 'Build a control from its source folder (local, or in the Zone; component.json + files) into a Workspace; instances update, a failure commits nothing',
494
- description: 'The folder is a local folder (when it exists here; its files are sent inline, ≤ 4 MiB) or a Zone path (e.g. /home/alice/AI Workspace/Demo/controls/kpi-card; --zone-path forces it), flat. The base of the last build or pull is kept in its .aiws-build.json; building over a newer Workspace version is refused (pull first, or --force). --watch rebuilds on every change until interrupted.',
540
+ description: 'The folder is a local folder (when it exists here; its files are sent inline, and past 4 MiB per build the largest are uploaded to your exports bucket and sent by reference) or a Zone path (e.g. /home/alice/AI Workspace/Demo/controls/kpi-card; --zone-path forces it); sub-folders are part of the source (file names are relative paths). The base of the last build or pull is kept in its .aiws-build.json; building over a newer Workspace version is refused (pull first, or --force). --watch rebuilds on every change until interrupted. A build that narrows what instances rely on (a binding renamed, a slot moved, an event dropped) takes --migration, a JSON file { "<author_did>#<name>": Migration } (as ext-upgrade).',
495
541
  positionals: [{ name: 'dir', description: 'Local folder, or Zone path of the control source folder', required: true }],
496
542
  options: [
497
543
  WORKSPACE_OPTION,
@@ -500,8 +546,9 @@ const buildCommand: CommandDefinition = {
500
546
  { name: 'dry-run', property: 'dry_run', description: 'Check only: diagnostics, nothing committed', type: 'boolean' },
501
547
  { name: 'force', property: 'force', description: 'Build over whatever the Workspace holds now', type: 'boolean' },
502
548
  { name: 'zone-path', property: 'zone_path', description: 'Treat the folder as a Zone path even if a local folder has that name', type: 'boolean' },
549
+ { name: 'migration', property: 'migration', description: 'JSON file { "<author_did>#<name>": Migration } rewriting the instances of a build that narrows the contract', type: 'string' },
503
550
  ],
504
- inputSchema: workspaceInput({ dir: { type: 'string', minLength: 1 }, folder_id: { type: 'string' }, watch: { type: 'boolean' }, dry_run: { type: 'boolean' }, force: { type: 'boolean' }, zone_path: { type: 'boolean' } }, ['dir']),
551
+ inputSchema: workspaceInput({ dir: { type: 'string', minLength: 1 }, folder_id: { type: 'string' }, watch: { type: 'boolean' }, dry_run: { type: 'boolean' }, force: { type: 'boolean' }, zone_path: { type: 'boolean' }, migration: { type: 'string' } }, ['dir']),
505
552
  outputSchema: OBJECT_OUTPUT,
506
553
  resultSchemaVersion: 1,
507
554
  access: { mode: 'fixed', level: 'write' },
@@ -511,7 +558,8 @@ const buildCommand: CommandDefinition = {
511
558
  handler: async (ctx, input) => {
512
559
  const ws = requiredInputString(input, 'workspace_id')
513
560
  const dir = requiredInputString(input, 'dir')
514
- const opts = { folderId: inputString(input, 'folder_id'), force: input.force === true, dryRun: input.dry_run === true }
561
+ const migration = inputString(input, 'migration')
562
+ const opts = { folderId: inputString(input, 'folder_id'), force: input.force === true, dryRun: input.dry_run === true, upload: sourceUploader(ctx), ...(migration ? { migration: await migrationFile(ctx, migration) } : {}) }
515
563
  if (input.zone_path !== true && await isLocalDir(getHost().path.resolve(ctx.cwd, dir))) {
516
564
  const local = getHost().path.resolve(ctx.cwd, dir)
517
565
  if (input.watch === true) throw new UsageError('AIWS_USAGE', '--watch follows a Zone folder; rebuild a local folder yourself')
@@ -559,7 +607,8 @@ const pullCommand: CommandDefinition = {
559
607
  const fetchAsset = assetFetcher(ctx)
560
608
  const files: string[] = []
561
609
  for (const f of control.files as { name: string; obj_id?: string; text?: string }[]) {
562
- const target = host.path.join(out, f.name)
610
+ const target = host.path.join(out, ...f.name.split('/'))
611
+ await host.mkdir(host.path.dirname(target), { recursive: true })
563
612
  if (f.text !== undefined) await host.writeTextFile(target, f.text)
564
613
  else await host.writeFile(target, await fetchAsset(ws, String(f.obj_id)))
565
614
  files.push(f.name)
@@ -593,6 +642,64 @@ async function uploadPackage(ctx: CommandContext, path: string): Promise<{ bucke
593
642
  return { bucket_id, key }
594
643
  }
595
644
 
645
+ const PACKAGE_INPUT: Record<string, JsonSchema> = { name: { type: 'string' }, version: { type: 'string' }, owner: { type: 'string' }, pikg: { type: 'boolean' }, install: { type: 'boolean' } }
646
+
647
+ function packageOptions(nameDefault: string): OptionDefinition[] {
648
+ return [
649
+ { name: 'name', property: 'name', description: `Package name (default: ${nameDefault})`, type: 'string' },
650
+ { name: 'version', property: 'version', description: 'Package version (default 0.1.0)', type: 'string' },
651
+ { name: 'owner', property: 'owner', description: 'Owner DID of the package (default did:bns:<you>)', type: 'string' },
652
+ { name: 'pikg', property: 'pikg', description: 'Also pack the bundle as an aiws PIKG', type: 'boolean' },
653
+ { name: 'install', property: 'install', description: 'Pack, then install into your AI Workspace library (dev mode)', type: 'boolean' },
654
+ ]
655
+ }
656
+
657
+ const fromBase64 = (text: string) => Uint8Array.from(atob(text), (c) => c.charCodeAt(0))
658
+
659
+ /** Write a bundle (manifest, `controls/<dir>/`, `templates/<id>/`) as `<out>/aiws/`. */
660
+ async function writeBundle(out: string, files: Map<string, Uint8Array>): Promise<string> {
661
+ const host = getHost()
662
+ const root = host.path.join(out, 'aiws')
663
+ // a previous export's controls must not linger in the bundle
664
+ await host.remove(root, { recursive: true }).catch(() => undefined)
665
+ for (const [path, bytes] of files) {
666
+ const target = host.path.join(root, path)
667
+ await host.mkdir(host.path.dirname(target), { recursive: true })
668
+ await host.writeFile(target, bytes)
669
+ }
670
+ return root
671
+ }
672
+
673
+ /** `--pikg` / `--install` of a bundle written into `<out>/aiws/`: pikg init (once) / build / pack in `<out>` (kind
674
+ * aiws-controls); `--install` binds the `.pikg` into your aiworkspace exports bucket and calls `ext.install`. */
675
+ async function packBundle(ctx: CommandContext, input: Record<string, unknown>, out: string, defaultName: string, result: Record<string, unknown>): Promise<Record<string, unknown>> {
676
+ if (input.pikg !== true && input.install !== true) return result
677
+ const host = getHost()
678
+ const name = inputString(input, 'name') ?? defaultName
679
+ const owner = inputString(input, 'owner') ?? `did:bns:${ctx.principal.id}`
680
+ const version = inputString(input, 'version') ?? '0.1.0'
681
+ const appJson = host.path.join(out, 'dapp_meta', 'app.json')
682
+ const existing = await host.readTextFile(appJson).then((t) => JSON.parse(t) as Record<string, unknown>).catch(() => null)
683
+ if (!existing) {
684
+ await pikgVerb(ctx, 'init', { project_dir: out, name, owner, kind: 'aiws-controls', source: './aiws', version })
685
+ } else if (inputString(input, 'version') && existing.version !== version) {
686
+ // a re-export at a new version: the same project, the new version (a library refuses other content at an old one)
687
+ await host.writeTextFile(appJson, `${JSON.stringify({ ...existing, version }, null, 2)}\n`)
688
+ const pikgJson = host.path.join(out, 'dapp_meta', 'pikg.json')
689
+ const meta = JSON.parse(await host.readTextFile(pikgJson)) as Record<string, unknown>
690
+ await host.writeTextFile(pikgJson, `${JSON.stringify({ ...meta, pikg_file: `${existing.name}-${version}.pikg` }, null, 2)}\n`)
691
+ }
692
+ await pikgVerb(ctx, 'build', { meta_dir: host.path.join(out, 'dapp_meta') })
693
+ const packed = await pikgVerb(ctx, 'pack', { dist_dir: host.path.join(out, 'dapp_dist') })
694
+ const pikgPath = String(packed.pikg_path)
695
+ result.pikg = pikgPath
696
+ if (input.install !== true) return result
697
+ result.install = await aiws(ctx, 'ext.install', { pikg: await uploadPackage(ctx, pikgPath), mode: 'dev' })
698
+ return result
699
+ }
700
+
701
+ const controlSummary = (controls: Record<string, unknown>[]) => controls.map((c) => ({ ident: c.ident, kind: c.kind, version: c.version, digest: c.digest }))
702
+
596
703
  const extExportCommand: CommandDefinition = {
597
704
  verb: 'ext-export',
598
705
  summary: 'Export Workspace controls as a bundle in a local folder; optionally pack it as an aiws PIKG and install it into your library',
@@ -601,13 +708,9 @@ const extExportCommand: CommandDefinition = {
601
708
  options: [
602
709
  WORKSPACE_OPTION,
603
710
  { name: 'out', property: 'out', description: 'Local folder to write into (created)', type: 'string', required: true },
604
- { name: 'name', property: 'name', description: 'Package name (default: the first control’s)', type: 'string' },
605
- { name: 'version', property: 'version', description: 'Package version (default 0.1.0)', type: 'string' },
606
- { name: 'owner', property: 'owner', description: 'Owner DID of the package (default did:bns:<you>)', type: 'string' },
607
- { name: 'pikg', property: 'pikg', description: 'Also pack the bundle as an aiws PIKG', type: 'boolean' },
608
- { name: 'install', property: 'install', description: 'Pack, then install into your AI Workspace library (dev mode)', type: 'boolean' },
711
+ ...packageOptions('the first control’s'),
609
712
  ],
610
- inputSchema: workspaceInput({ defs: { type: 'string', minLength: 1 }, out: { type: 'string', minLength: 1 }, name: { type: 'string' }, version: { type: 'string' }, owner: { type: 'string' }, pikg: { type: 'boolean' }, install: { type: 'boolean' } }, ['defs', 'out']),
713
+ inputSchema: workspaceInput({ defs: { type: 'string', minLength: 1 }, out: { type: 'string', minLength: 1 }, ...PACKAGE_INPUT }, ['defs', 'out']),
611
714
  outputSchema: OBJECT_OUTPUT,
612
715
  resultSchemaVersion: 1,
613
716
  access: { mode: 'fixed', level: 'write' },
@@ -615,93 +718,294 @@ const extExportCommand: CommandDefinition = {
615
718
  requiresSession: true,
616
719
  examples: ['buckyos aiws ext-export def_abc,def_xyz --workspace ws_abc --out ./kpi-pack --install'],
617
720
  handler: async (ctx, input) => {
618
- const ws = requiredInputString(input, 'workspace_id')
619
- const host = getHost()
620
- const out = host.path.resolve(ctx.cwd, requiredInputString(input, 'out'))
621
- const bundle = await exportBundle(extRpc(ctx), ws, list(input, 'defs'), assetFetcher(ctx))
622
- const root = host.path.join(out, 'aiws')
623
- // a previous export's controls must not linger in the bundle
624
- await host.remove(root, { recursive: true }).catch(() => undefined)
625
- for (const [path, bytes] of bundle.files) {
626
- const target = host.path.join(root, path)
627
- await host.mkdir(host.path.dirname(target), { recursive: true })
628
- await host.writeFile(target, bytes)
629
- }
630
- const result: Record<string, unknown> = { ok: true, bundle: root, controls: bundle.manifest.controls.map((c: Record<string, unknown>) => ({ ident: c.ident, kind: c.kind, version: c.version, digest: c.digest })) }
631
- if (input.pikg !== true && input.install !== true) return result
632
- const name = inputString(input, 'name') ?? bundle.manifest.controls[0]?.name.replace(/[._]/g, '-') ?? 'controls'
633
- const owner = inputString(input, 'owner') ?? `did:bns:${ctx.principal.id}`
634
- const version = inputString(input, 'version') ?? '0.1.0'
635
- const appJson = host.path.join(out, 'dapp_meta', 'app.json')
636
- const existing = await host.readTextFile(appJson).then((t) => JSON.parse(t) as Record<string, unknown>).catch(() => null)
637
- if (!existing) {
638
- await pikgVerb(ctx, 'init', { project_dir: out, name, owner, kind: 'aiws-controls', source: './aiws', version })
639
- } else if (inputString(input, 'version') && existing.version !== version) {
640
- // a re-export at a new version: the same project, the new version (a library refuses other content at an old one)
641
- await host.writeTextFile(appJson, `${JSON.stringify({ ...existing, version }, null, 2)}\n`)
642
- const pikgJson = host.path.join(out, 'dapp_meta', 'pikg.json')
643
- const meta = JSON.parse(await host.readTextFile(pikgJson)) as Record<string, unknown>
644
- await host.writeTextFile(pikgJson, `${JSON.stringify({ ...meta, pikg_file: `${existing.name}-${version}.pikg` }, null, 2)}\n`)
645
- }
646
- await pikgVerb(ctx, 'build', { meta_dir: host.path.join(out, 'dapp_meta') })
647
- const packed = await pikgVerb(ctx, 'pack', { dist_dir: host.path.join(out, 'dapp_dist') })
648
- const pikgPath = String(packed.pikg_path)
649
- result.pikg = pikgPath
650
- if (input.install !== true) return result
651
- result.install = await aiws(ctx, 'ext.install', { pikg: await uploadPackage(ctx, pikgPath), mode: 'dev' })
652
- return result
721
+ const out = getHost().path.resolve(ctx.cwd, requiredInputString(input, 'out'))
722
+ const bundle = await exportBundle(extRpc(ctx), requiredInputString(input, 'workspace_id'), list(input, 'defs'), assetFetcher(ctx))
723
+ const result: Record<string, unknown> = { ok: true, bundle: await writeBundle(out, bundle.files), controls: controlSummary(bundle.manifest.controls) }
724
+ return packBundle(ctx, input, out, bundle.manifest.controls[0]?.name.replace(/[._]/g, '-') ?? 'controls', result)
725
+ },
726
+ }
727
+
728
+ const extUpgradeCommand: CommandDefinition = {
729
+ verb: 'ext-upgrade',
730
+ summary: 'Upgrade controls a Workspace imported from your library to the version your library holds now (one Commit; instances keep their overrides and content)',
731
+ description: 'Each definition is rebuilt from its package in your library (--pkg, or the package it was imported from), with what it builds on from the same package, dependencies first. A definition built locally since its import is refused (EXT_LOCAL_CHANGES; the error data lists the changed files) unless --force. A version that narrows what instances rely on needs --migration, a JSON file { "<author_did>#<name>": Migration } keyed by identity (config / bindings { rename, drop, fill }, slots { rename, move }, events { rename, drop }); --dry-run answers what it would write on each instance. Across Zones: `aiws library bundle <pkg_id> --out <file>` in the Zone of the library, then `aiws ext-import --bundle <file> --upgrade` in the Zone of the Workspace.',
732
+ positionals: [{ name: 'defs', description: 'Definition (buckyos.block-def) ids, comma-separated', required: true }],
733
+ options: [
734
+ WORKSPACE_OPTION,
735
+ { name: 'pkg', property: 'pkg_id', description: 'Library package to upgrade from (default: the one each definition was imported from)', type: 'string' },
736
+ { name: 'migration', property: 'migration', description: 'JSON file { "<author_did>#<name>": Migration } rewriting the instances of a narrowing version', type: 'string' },
737
+ { name: 'dry-run', property: 'dry_run', description: 'Check only: the plan per instance, nothing committed', type: 'boolean' },
738
+ { name: 'force', property: 'force', description: 'Upgrade over local changes (they are replaced)', type: 'boolean' },
739
+ ],
740
+ inputSchema: workspaceInput({ defs: { type: 'string', minLength: 1 }, pkg_id: { type: 'string' }, migration: { type: 'string' }, dry_run: { type: 'boolean' }, force: { type: 'boolean' } }, ['defs']),
741
+ outputSchema: OBJECT_OUTPUT,
742
+ resultSchemaVersion: 1,
743
+ access: { mode: 'fixed', level: 'write' },
744
+ asyncMode: 'sync',
745
+ requiresSession: true,
746
+ examples: ['buckyos aiws ext-upgrade def_abc --workspace ws_abc --dry-run', 'buckyos aiws ext-upgrade def_abc,def_xyz --workspace ws_abc --migration ./migration.json'],
747
+ handler: async (ctx, input) => {
748
+ const pkg = inputString(input, 'pkg_id')
749
+ const migration = inputString(input, 'migration')
750
+ return aiws(ctx, 'ext.upgrade', {
751
+ workspace_id: requiredInputString(input, 'workspace_id'),
752
+ items: list(input, 'defs').map((def_entity_id) => ({ def_entity_id, ...(pkg ? { pkg_id: pkg } : {}) })),
753
+ ...(migration ? { migration: await migrationFile(ctx, migration) } : {}),
754
+ ...(input.force === true ? { force: true } : {}), ...(input.dry_run === true ? { dry_run: true } : {}),
755
+ ...(ctx.idempotencyKey ? { idempotency_key: ctx.idempotencyKey } : {}),
756
+ })
653
757
  },
654
758
  }
655
759
 
656
760
  const libraryCommand: CommandDefinition = {
657
761
  verb: 'library',
658
- summary: 'Your AI Workspace control library: `list`, `install <file.pikg>` (an aiws package, dev mode), `remove <pkg_id>`',
659
- positionals: [{ name: 'action', description: 'list | install | remove', required: true }, { name: 'target', description: 'install: local .pikg path; remove: package id', required: false }],
660
- options: [],
661
- inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['list', 'install', 'remove'] }, target: { type: 'string' } }, required: ['action'], additionalProperties: false },
762
+ summary: 'Your AI Workspace control library: `list` (each control with the identities it depends on, `deps`, and the package’s templates), `install <file.pikg>` (an aiws package, dev mode), `remove <pkg_id>`, `bundle <pkg_id> --out <file.json>` (the controls with their bytes, for `ext-import --bundle` in another Zone)',
763
+ positionals: [{ name: 'action', description: 'list | install | remove | bundle', required: true }, { name: 'target', description: 'install: local .pikg path; remove, bundle: package id', required: false }],
764
+ options: [
765
+ { name: 'out', property: 'out', description: 'bundle: the JSON file to write', type: 'string' },
766
+ { name: 'idents', property: 'idents', description: 'bundle: only these controls (and what they build on), comma-separated identities', type: 'string' },
767
+ ],
768
+ inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['list', 'install', 'remove', 'bundle'] }, target: { type: 'string' }, out: { type: 'string' }, idents: { type: 'string' } }, required: ['action'], additionalProperties: false },
662
769
  outputSchema: OBJECT_OUTPUT,
663
770
  resultSchemaVersion: 1,
664
771
  access: { mode: 'fixed', level: 'write' },
665
772
  asyncMode: 'sync',
666
773
  requiresSession: true,
667
- examples: ['buckyos aiws library list', 'buckyos aiws library install ./kpi-pack-0.1.0.pikg', 'buckyos aiws library remove kpi-pack.alice.bns.did'],
774
+ examples: ['buckyos aiws library list', 'buckyos aiws library install ./kpi-pack-0.1.0.pikg', 'buckyos aiws library remove kpi-pack.alice.bns.did', 'buckyos aiws library bundle kpi-pack.alice.bns.did --out ./kpi-pack.bundle.json'],
668
775
  handler: async (ctx, input) => {
669
776
  const action = requiredInputString(input, 'action')
670
777
  if (action === 'list') return aiws(ctx, 'ext.list_library', {})
671
778
  if (action === 'remove') return aiws(ctx, 'ext.uninstall', { pkg_id: requiredInputString(input, 'target') })
672
- if (action !== 'install') throw new UsageError('AIWS_USAGE', 'library: list, install <file.pikg> or remove <pkg_id>')
779
+ if (action === 'bundle') {
780
+ const host = getHost()
781
+ const out = host.path.resolve(ctx.cwd, requiredInputString(input, 'out'))
782
+ const idents = inputString(input, 'idents')
783
+ const r = await aiws(ctx, 'ext.library_bundle', { pkg_id: requiredInputString(input, 'target'), ...(idents ? { idents: list(input, 'idents') } : {}) })
784
+ const controls = (Array.isArray(r.controls) ? r.controls : []) as Record<string, unknown>[]
785
+ await host.writeTextFile(out, `${JSON.stringify({ pkg_id: r.pkg_id, controls }, null, 2)}\n`)
786
+ return { ok: true, out, pkg_id: r.pkg_id, controls: controls.map((c) => ({ ident: c.ident, version: c.version, digest: c.digest, deps: c.deps })) }
787
+ }
788
+ if (action !== 'install') throw new UsageError('AIWS_USAGE', 'library: list, install <file.pikg>, remove <pkg_id> or bundle <pkg_id> --out <file.json>')
673
789
  const path = getHost().path.resolve(ctx.cwd, requiredInputString(input, 'target'))
674
790
  return aiws(ctx, 'ext.install', { pikg: await uploadPackage(ctx, path), mode: 'dev' })
675
791
  },
676
792
  }
677
793
 
794
+ interface BundleControl { ident: string; deps?: string[]; provenance?: Record<string, unknown>; files: { name: string; base64: string }[] }
795
+
796
+ /** Build the controls of a library bundle (`library bundle` in the Zone of the library) into a Workspace of this Zone:
797
+ * their bytes with their `provenance`, what they build on first (README “Import” across Zones). `upgrade` replaces what
798
+ * the Workspace holds of these identities (`migration` per identity, Q30); without it a dependency the Workspace already
799
+ * holds from another source is met by that definition, as `ext.import` does. */
800
+ async function relayBundle(ctx: CommandContext, ws: string, controls: BundleControl[], opts: { folderId?: string; upgrade: boolean; migration?: Record<string, unknown> }): Promise<Record<string, unknown>[]> {
801
+ const pending = [...controls]
802
+ const needed = new Set(controls.flatMap((c) => c.deps ?? []))
803
+ const out: Record<string, unknown>[] = []
804
+ while (pending.length > 0) {
805
+ const left = new Set(pending.map((c) => c.ident))
806
+ const c = pending.splice(Math.max(0, pending.findIndex((p) => (p.deps ?? []).every((d) => !left.has(d) || d === p.ident))), 1)[0]
807
+ try {
808
+ const r = await aiws(ctx, 'ext.build', {
809
+ workspace_id: ws, files: await sourceErrors(() => packBuildFiles(c.files.map((f) => ({ name: f.name, bytes: fromBase64(f.base64) })), sourceUploader(ctx))), provenance: c.provenance,
810
+ ...(opts.folderId ? { folder_id: opts.folderId } : {}),
811
+ ...(opts.upgrade ? { upgrade: true } : {}),
812
+ ...(opts.migration?.[c.ident] ? { migration: { [c.ident]: opts.migration[c.ident] } } : {}),
813
+ })
814
+ out.push({ ident: c.ident, def_entity_id: r.def_entity_id, action: r.action ?? r.status, version: r.version })
815
+ } catch (error) {
816
+ if (opts.upgrade || !needed.has(c.ident) || !(error instanceof ToolError) || error.details.sub_code !== 'EXT_IDENT_EXISTS') throw error
817
+ out.push({ ident: c.ident, def_entity_id: (error.details.data as { def_entity_id?: unknown } | undefined)?.def_entity_id, action: 'satisfied' })
818
+ }
819
+ }
820
+ return out
821
+ }
822
+
678
823
  const extImportCommand: CommandDefinition = {
679
824
  verb: 'ext-import',
680
- summary: 'Bring controls of your library into a Workspace (one Commit): <pkg_id>#<ident>, comma-separated',
681
- positionals: [{ name: 'items', description: '<pkg_id>#<author_did>#<name>[,…] (ext.list_library names them)', required: true }],
682
- options: [WORKSPACE_OPTION, { name: 'folder', property: 'folder_id', description: 'Data folder for the definitions (default: data/extensions)', type: 'string' }],
683
- inputSchema: workspaceInput({ items: { type: 'string', minLength: 1 }, folder_id: { type: 'string' } }, ['items']),
825
+ summary: 'Bring controls of your library into a Workspace (one Commit): <pkg_id>#<ident>, comma-separated; what they build on (deps) comes along, dependencies first, and an identity the Workspace already has satisfies it. --bundle builds a library bundle of another Zone instead',
826
+ description: 'Across Zones: `aiws library bundle <pkg_id> --out <file>` in the Zone of the library, then `aiws ext-import --bundle <file> --workspace <ws>` in the Zone of the Workspace: each control is built with its package provenance, dependencies first; a dependency the Workspace already holds is met by that definition. --upgrade (with --bundle) upgrades the identities the Workspace holds from another version, --migration naming a JSON file { "<author_did>#<name>": Migration } for a version that narrows what instances rely on. Upgrades from your library in the same Zone are `aiws ext-upgrade`.',
827
+ positionals: [{ name: 'items', description: '<pkg_id>#<author_did>#<name>[,…] (ext.list_library names them); not with --bundle', required: false }],
828
+ options: [
829
+ WORKSPACE_OPTION,
830
+ { name: 'folder', property: 'folder_id', description: 'Data folder for the definitions (default: data/extensions)', type: 'string' },
831
+ { name: 'bundle', property: 'bundle', description: 'A library bundle file (`aiws library bundle`, any Zone) to build instead of items', type: 'string' },
832
+ { name: 'upgrade', property: 'upgrade', description: 'With --bundle: upgrade identities the Workspace holds from another version', type: 'boolean' },
833
+ { name: 'migration', property: 'migration', description: 'With --upgrade: JSON file { "<author_did>#<name>": Migration }', type: 'string' },
834
+ ],
835
+ inputSchema: workspaceInput({ items: { type: 'string', minLength: 1 }, folder_id: { type: 'string' }, bundle: { type: 'string' }, upgrade: { type: 'boolean' }, migration: { type: 'string' } }),
684
836
  outputSchema: OBJECT_OUTPUT,
685
837
  resultSchemaVersion: 1,
686
838
  access: { mode: 'fixed', level: 'write' },
687
839
  asyncMode: 'sync',
688
840
  requiresSession: true,
689
- examples: ['buckyos aiws ext-import kpi-pack.alice.bns.did#did:bns:alice#kpi-card --workspace ws_abc'],
690
- handler: (ctx, input) => {
691
- const items = list(input, 'items').map((item) => {
692
- const cut = item.indexOf('#')
693
- if (cut <= 0) throw new UsageError('AIWS_USAGE', `${item}: <pkg_id>#<ident>`)
694
- return { pkg_id: item.slice(0, cut), ident: item.slice(cut + 1) }
695
- })
841
+ examples: ['buckyos aiws ext-import kpi-pack.alice.bns.did#did:bns:alice#kpi-card --workspace ws_abc', 'buckyos aiws ext-import --bundle ./kpi-pack.bundle.json --workspace ws_abc --upgrade'],
842
+ handler: async (ctx, input) => {
843
+ const ws = requiredInputString(input, 'workspace_id')
696
844
  const folder = inputString(input, 'folder_id')
697
- return aiws(ctx, 'ext.import', { workspace_id: requiredInputString(input, 'workspace_id'), items, ...(folder ? { folder_id: folder } : {}) })
845
+ const bundle = inputString(input, 'bundle')
846
+ const migration = inputString(input, 'migration')
847
+ if (migration && input.upgrade !== true) throw new UsageError('AIWS_USAGE', '--migration goes with --upgrade')
848
+ if (!bundle) {
849
+ if (input.upgrade === true) throw new UsageError('AIWS_USAGE', '--upgrade goes with --bundle (a library bundle of another Zone); upgrade what came from your own library with `aiws ext-upgrade`')
850
+ const items = list(input, 'items').map((item) => {
851
+ const cut = item.indexOf('#')
852
+ if (cut <= 0) throw new UsageError('AIWS_USAGE', `${item}: <pkg_id>#<ident>`)
853
+ return { pkg_id: item.slice(0, cut), ident: item.slice(cut + 1) }
854
+ })
855
+ return aiws(ctx, 'ext.import', { workspace_id: ws, items, ...(folder ? { folder_id: folder } : {}) })
856
+ }
857
+ if (inputString(input, 'items')) throw new UsageError('AIWS_USAGE', 'pass items or --bundle, not both')
858
+ const host = getHost()
859
+ const file = JSON.parse(await host.readTextFile(host.path.resolve(ctx.cwd, bundle))) as { controls?: unknown }
860
+ if (!Array.isArray(file.controls) || file.controls.some((c) => !c || typeof c.ident !== 'string' || !Array.isArray(c.files))) {
861
+ throw new UsageError('AIWS_USAGE', `--bundle ${bundle}: not a library bundle ({ pkg_id, controls: [{ ident, provenance, files }] }, from \`aiws library bundle\`)`)
862
+ }
863
+ const controls = await relayBundle(ctx, ws, file.controls as BundleControl[], { folderId: folder, upgrade: input.upgrade === true, ...(migration ? { migration: await migrationFile(ctx, migration) } : {}) })
864
+ return { ok: true, controls }
865
+ },
866
+ }
867
+
868
+ /** `template create --param k=v[,k=v…]`, typed by the template's `params` (string | number | boolean). */
869
+ function createParams(text: string, declared: Record<string, { type?: string }>): Record<string, string | number | boolean> {
870
+ const out: Record<string, string | number | boolean> = {}
871
+ for (const pair of text.split(',').map((s) => s.trim()).filter(Boolean)) {
872
+ const cut = pair.indexOf('=')
873
+ const name = cut > 0 ? pair.slice(0, cut) : ''
874
+ if (!Object.hasOwn(declared, name)) throw new UsageError('AIWS_USAGE', `--param ${pair}: the template's params are ${Object.keys(declared).join(', ') || '(none)'}`)
875
+ out[name] = typedValue(declared[name].type, pair.slice(cut + 1), `--param ${pair}`)
876
+ }
877
+ return out
878
+ }
879
+
880
+ /** `template export --param name:type[:default][,…]`: the params a new Workspace is created with. */
881
+ function exportParams(text: string): Record<string, { type: string; default?: string | number | boolean }> {
882
+ const out: Record<string, { type: string; default?: string | number | boolean }> = {}
883
+ for (const item of text.split(',').map((s) => s.trim()).filter(Boolean)) {
884
+ const [name, type, ...rest] = item.split(':')
885
+ if (!name || !['string', 'number', 'boolean'].includes(type)) throw new UsageError('AIWS_USAGE', `--param ${item}: <name>:<string | number | boolean>[:<default>]`)
886
+ out[name] = { type, ...(rest.length > 0 ? { default: typedValue(type, rest.join(':'), `--param ${item}`) } : {}) }
887
+ }
888
+ return out
889
+ }
890
+
891
+ function typedValue(type: string | undefined, raw: string, label: string): string | number | boolean {
892
+ if (type === 'number') {
893
+ const n = Number(raw)
894
+ if (!raw.trim() || !Number.isFinite(n)) throw new UsageError('AIWS_USAGE', `${label}: a number`)
895
+ return n
896
+ }
897
+ if (type === 'boolean') {
898
+ if (raw !== 'true' && raw !== 'false') throw new UsageError('AIWS_USAGE', `${label}: true or false`)
899
+ return raw === 'true'
900
+ }
901
+ return raw
902
+ }
903
+
904
+ /** `template export`: the template snapshot (`ext.export_template`) and the controls it needs (`ext.export`, package)
905
+ * as one bundle in `<out>/aiws/`, then packed / installed as `ext-export` does. */
906
+ async function exportTemplate(ctx: CommandContext, input: Record<string, unknown>): Promise<Record<string, unknown>> {
907
+ const ws = requiredInputString(input, 'workspace_id')
908
+ const id = requiredInputString(input, 'id')
909
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(id)) throw new UsageError('AIWS_USAGE', `--id ${id}: [a-z0-9][a-z0-9-]{0,63}`)
910
+ const description = inputString(input, 'description')
911
+ const param = inputString(input, 'param')
912
+ const template = {
913
+ id, title: requiredInputString(input, 'title'), ...(description ? { description } : {}), ...(param ? { params: exportParams(param) } : {}),
914
+ ...(inputString(input, 'reset') ? { reset: { tables: list(input, 'reset') } } : {}),
915
+ }
916
+ const out = getHost().path.resolve(ctx.cwd, requiredInputString(input, 'out'))
917
+ const r = await aiws(ctx, 'ext.export_template', { workspace_id: ws, surfaces: list(input, 'surfaces'), ...(inputString(input, 'data') ? { data: list(input, 'data') } : {}), template })
918
+ const item = expectObject(r.template, 'ext.export_template.template')
919
+ const defs = (Array.isArray(r.controls) ? r.controls : []).map((c) => typeof c === 'string' ? c : String(expectObject(c, 'ext.export_template.controls[]').entity_id))
920
+ const files = new Map<string, Uint8Array>()
921
+ let manifest: Record<string, unknown> = { manifest_version: AIWS_MANIFEST_VERSION, purpose: 'aiws', host_api: {}, controls: [] }
922
+ if (defs.length > 0) {
923
+ const bundle = await exportBundle(extRpc(ctx), ws, defs, assetFetcher(ctx))
924
+ for (const [path, bytes] of bundle.files) files.set(path, bytes)
925
+ manifest = bundle.manifest
926
+ }
927
+ const dir = templateDir(id)
928
+ for (const f of (Array.isArray(r.files) ? r.files : []) as { path: string; base64: string }[]) {
929
+ const path = f.path.startsWith(dir) ? f.path : `${dir}${f.path}`
930
+ if (!isSourcePath(path)) throw new ToolError('INVALID_SERVICE_RESPONSE', `ext.export_template: ${f.path} is not a file of the template`)
931
+ files.set(path, fromBase64(f.base64))
932
+ }
933
+ manifest = { ...manifest, manifest_version: AIWS_MANIFEST_VERSION, workspace_templates: [item] }
934
+ files.set(AIWS_MANIFEST_FILE, new TextEncoder().encode(`${JSON.stringify(manifest, null, 2)}\n`))
935
+ const result: Record<string, unknown> = {
936
+ ok: true, bundle: await writeBundle(out, files), template: { id: item.id, title: item.title, digest: item.digest, requires: item.requires },
937
+ controls: controlSummary(manifest.controls as Record<string, unknown>[]),
938
+ }
939
+ return packBundle(ctx, input, out, id, result)
940
+ }
941
+
942
+ const templateCommand: CommandDefinition = {
943
+ verb: 'template',
944
+ summary: 'Workspace templates (snapshots carried in control packages): `list` (the templates of your library), `create <pkg_id>#<template_id>` (a new Workspace from one: answers its workspace_id), `export` (Surfaces of a Workspace as a template bundle; --pikg / --install pack it with the controls it needs and install it)',
945
+ description: 'create: --title names the new Workspace; --param k=v[,k=v…] sets the template’s params (typed as the template declares them; `template list` shows them). Installing a template never creates a Workspace; each create makes an independent one. export: --workspace, --surfaces s1,s2, --id and --title are required; --data e1,e2 carries data entities along, --param name:type[:default][,…] (type string | number | boolean) declares what a creator fills into {{params.<name>}}, --reset t1,t2 names tables a new Workspace starts without rows. It writes <out>/aiws/ (aiws-manifest.json at manifest_version 3 with the template in workspace_templates, controls/<dir>/ of the controls it needs, templates/<id>/), then packs and installs it as `ext-export` does.',
946
+ positionals: [{ name: 'action', description: 'list | create | export', required: true }, { name: 'target', description: 'create: <pkg_id>#<template_id>', required: false }],
947
+ options: [
948
+ { name: 'workspace', property: 'workspace_id', description: 'export: Workspace id (ws_…)', type: 'string' },
949
+ { name: 'surfaces', property: 'surfaces', description: 'export: Surface ids, comma-separated', type: 'string' },
950
+ { name: 'id', property: 'id', description: 'export: template id ([a-z0-9][a-z0-9-]{0,63})', type: 'string' },
951
+ { name: 'title', property: 'title', description: 'create: title of the new Workspace; export: title of the template', type: 'string' },
952
+ { name: 'description', property: 'description', description: 'export: what the template is for', type: 'string' },
953
+ { name: 'data', property: 'data', description: 'export: data entity ids to carry along, comma-separated', type: 'string' },
954
+ { name: 'param', property: 'param', description: 'create: k=v[,k=v…]; export: name:type[:default][,…]', type: 'string' },
955
+ { name: 'reset', property: 'reset', description: 'export: tables a new Workspace starts without rows, comma-separated', type: 'string' },
956
+ { name: 'out', property: 'out', description: 'export: local folder to write into (created)', type: 'string' },
957
+ ...packageOptions('the template id'),
958
+ ],
959
+ inputSchema: {
960
+ type: 'object',
961
+ properties: {
962
+ action: { type: 'string', enum: ['list', 'create', 'export'] }, target: { type: 'string' }, workspace_id: { type: 'string' }, surfaces: { type: 'string' }, id: { type: 'string' },
963
+ title: { type: 'string' }, description: { type: 'string' }, data: { type: 'string' }, param: { type: 'string' }, reset: { type: 'string' }, out: { type: 'string' }, ...PACKAGE_INPUT,
964
+ },
965
+ required: ['action'],
966
+ additionalProperties: false,
967
+ },
968
+ outputSchema: OBJECT_OUTPUT,
969
+ resultSchemaVersion: 1,
970
+ access: { mode: 'fixed', level: 'write' },
971
+ asyncMode: 'sync',
972
+ requiresSession: true,
973
+ examples: [
974
+ 'buckyos aiws template list',
975
+ 'buckyos aiws template create team-pack.alice.bns.did#team-board --title "Ops board" --param team_name=Ops',
976
+ 'buckyos aiws template export --workspace ws_abc --surfaces sf_board --id team-board --title "Team task board" --param team_name:string:Team --reset tasks --out ./team-pack --install',
977
+ ],
978
+ handler: async (ctx, input) => {
979
+ const action = requiredInputString(input, 'action')
980
+ if (action === 'list') {
981
+ const library = await aiws(ctx, 'ext.list_library', {})
982
+ const packages = (Array.isArray(library.packages) ? library.packages : []) as { pkg_id: string; version?: string; templates?: Record<string, unknown>[] }[]
983
+ return { ok: true, templates: packages.flatMap((p) => (p.templates ?? []).map((t) => ({ ref: `${p.pkg_id}#${t.id}`, pkg_id: p.pkg_id, version: p.version, ...t }))) }
984
+ }
985
+ if (action === 'export') return exportTemplate(ctx, input)
986
+ if (action !== 'create') throw new UsageError('AIWS_USAGE', 'template: list, create <pkg_id>#<template_id> or export --workspace <ws> --surfaces <ids> --id <id> --title <title> --out <dir>')
987
+ const target = requiredInputString(input, 'target')
988
+ const cut = target.indexOf('#')
989
+ if (cut <= 0 || cut === target.length - 1) throw new UsageError('AIWS_USAGE', `${target}: <pkg_id>#<template_id>`)
990
+ const [pkg_id, template_id] = [target.slice(0, cut), target.slice(cut + 1)]
991
+ const param = inputString(input, 'param')
992
+ let params: Record<string, string | number | boolean> | undefined
993
+ if (param) {
994
+ const library = await aiws(ctx, 'ext.list_library', {})
995
+ const packages = (Array.isArray(library.packages) ? library.packages : []) as { pkg_id: string; templates?: { id: string; params?: Record<string, { type?: string }> }[] }[]
996
+ const found = packages.find((p) => p.pkg_id === pkg_id)?.templates?.find((t) => t.id === template_id)
997
+ if (!found) throw new UsageError('AIWS_USAGE', `${target} is not a template of your library (aiws template list)`)
998
+ params = createParams(param, found.params ?? {})
999
+ }
1000
+ const title = inputString(input, 'title')
1001
+ return aiws(ctx, 'ext.create_from_template', { pkg_id, template_id, ...(title ? { title } : {}), ...(params ? { params } : {}) })
698
1002
  },
699
1003
  }
700
1004
 
701
1005
  export function createAiwsModule(): CommandModule {
702
1006
  return {
703
1007
  name: 'aiws',
704
- summary: 'AI Workspace: read a workspace, Block-controls (catalogue, import, export), extension controls (build, pull, export, library, import), and execute wish runs as their executor (claim, heartbeat, report)',
705
- commands: [infoCommand, readCommand, queryCommand, controlsCommand, importCommand, exportCommand, buildCommand, pullCommand, extExportCommand, libraryCommand, extImportCommand, runGetCommand, runClaimCommand, runHeartbeatCommand, runFilesCommand, runToolCommand, runProgramCommand, runReportCommand],
1008
+ summary: 'AI Workspace: read a workspace, Block-controls (catalogue, import, export), extension controls (build, pull, export, library, import, upgrade), Workspace templates (list, create, export), and execute wish runs as their executor (claim, heartbeat, report)',
1009
+ commands: [infoCommand, readCommand, queryCommand, controlsCommand, importCommand, exportCommand, buildCommand, pullCommand, extExportCommand, libraryCommand, extImportCommand, extUpgradeCommand, templateCommand, runGetCommand, runClaimCommand, runHeartbeatCommand, runFilesCommand, runToolCommand, runProgramCommand, runReportCommand],
706
1010
  }
707
1011
  }