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.
- package/cli/dist/cli.mjs +412 -96
- package/cli/dist/cli.mjs.map +1 -1
- package/cli/modules/aiws.ts +385 -81
- package/cli/modules/pikg.ts +15 -6
- package/cli/modules/pikg_protocol.ts +9 -3
- package/cli/version.ts +1 -1
- package/dist/aiworkspace_client.d.ts +165 -1
- package/dist/aiworkspace_client.d.ts.map +1 -1
- package/dist/aiws_ext.cjs +1 -1
- package/dist/aiws_ext.cjs.map +1 -1
- package/dist/aiws_ext.d.ts +160 -19
- package/dist/aiws_ext.d.ts.map +1 -1
- package/dist/aiws_ext.mjs +817 -68
- package/dist/aiws_ext.mjs.map +1 -1
- package/dist/browser.cjs +1 -1
- package/dist/browser.mjs +2 -2
- package/dist/{executor_client-50023bcc.mjs → executor_client-0856c3fc.mjs} +22 -2
- package/dist/executor_client-0856c3fc.mjs.map +1 -0
- package/dist/{executor_client-47edd42d.js → executor_client-3f7679e3.js} +9 -9
- package/dist/executor_client-3f7679e3.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +15 -3
- package/dist/node.cjs +1 -1
- package/dist/node.mjs +15 -3
- package/package.json +1 -1
- package/dist/executor_client-47edd42d.js.map +0 -1
- package/dist/executor_client-50023bcc.mjs.map +0 -1
package/cli/modules/aiws.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
454
|
-
async function localFiles(dir: string): Promise<{ name: 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;
|
|
484
|
+
const files: { name: string; bytes: Uint8Array }[] = []
|
|
457
485
|
const problems: { file: string; severity: 'error'; message: string }[] = []
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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.
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 },
|
|
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
|
|
619
|
-
const
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|