buckyos 0.7.159 → 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 +714 -20
- package/cli/dist/cli.mjs.map +1 -1
- package/cli/modules/aiws.ts +631 -4
- package/cli/modules/pikg.ts +38 -3
- package/cli/modules/pikg_protocol.ts +48 -3
- package/cli/modules/share.ts +2 -1
- package/cli/version.ts +1 -1
- package/dist/aiworkspace_client.d.ts +307 -0
- package/dist/aiworkspace_client.d.ts.map +1 -1
- package/dist/aiws_ext.cjs +2 -0
- package/dist/aiws_ext.cjs.map +1 -0
- package/dist/aiws_ext.d.ts +310 -0
- package/dist/aiws_ext.d.ts.map +1 -0
- package/dist/aiws_ext.mjs +1248 -0
- package/dist/aiws_ext.mjs.map +1 -0
- package/dist/browser.cjs +1 -1
- package/dist/browser.mjs +2 -2
- package/dist/{executor_client-24076936.mjs → executor_client-0856c3fc.mjs} +78 -1
- package/dist/executor_client-0856c3fc.mjs.map +1 -0
- package/dist/{executor_client-24abb248.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.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +48 -2
- package/dist/index.mjs.map +1 -1
- package/dist/node.cjs +1 -1
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.ts +2 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.mjs +50 -2
- package/dist/node.mjs.map +1 -1
- package/package.json +6 -1
- package/dist/executor_client-24076936.mjs.map +0 -1
- package/dist/executor_client-24abb248.js.map +0 -1
package/cli/modules/aiws.ts
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AIWS_MANIFEST_FILE,
|
|
3
|
+
AIWS_MANIFEST_VERSION,
|
|
4
|
+
AiwsSourceError,
|
|
5
|
+
buildControl,
|
|
6
|
+
exportBundle,
|
|
7
|
+
isSourcePath,
|
|
8
|
+
NfspClient,
|
|
9
|
+
packBuildFiles,
|
|
10
|
+
pullControl,
|
|
11
|
+
templateDir,
|
|
12
|
+
watchControl,
|
|
13
|
+
} from 'buckyos/node'
|
|
1
14
|
import type { CommandDefinition, CommandModule, JsonSchema, OptionDefinition, PositionalDefinition } from '../core/command.ts'
|
|
2
15
|
import type { CommandContext } from '../core/context.ts'
|
|
3
|
-
import { ToolError, UsageError } from '../core/errors.ts'
|
|
16
|
+
import { EXIT_AUTH, ToolError, UsageError } from '../core/errors.ts'
|
|
4
17
|
import { callService, expectObject, inputString, requiredInputString } from '../core/service.ts'
|
|
5
18
|
import { getHost } from '../runtime/host.ts'
|
|
19
|
+
import { createPikgModule } from './pikg.ts'
|
|
20
|
+
import { gatewayOrigin } from './share.ts'
|
|
6
21
|
|
|
7
22
|
// AI Workspace (`/kapi/aiworkspace`): reading a workspace, and the executor protocol of a wish run
|
|
8
23
|
// (内核架构改进 M3): claim → heartbeat → report. Source of truth for the methods:
|
|
@@ -23,7 +38,11 @@ async function aiws(ctx: CommandContext, method: string, params: Record<string,
|
|
|
23
38
|
const error = expectObject(result.error, `${method}.error`)
|
|
24
39
|
const code = typeof error.code === 'string' ? error.code : 'AIWS_ERROR'
|
|
25
40
|
const detail = typeof error.detail === 'string' ? error.detail : code
|
|
26
|
-
|
|
41
|
+
// the structured part (a build's diagnostics, a conflict's revisions) travels in the error's details
|
|
42
|
+
const details: Record<string, unknown> = {}
|
|
43
|
+
if (typeof error.sub_code === 'string') details.sub_code = error.sub_code
|
|
44
|
+
if (error.data !== undefined) details.data = error.data
|
|
45
|
+
throw new ToolError(code, `${code}${typeof error.sub_code === 'string' ? `/${error.sub_code}` : ''}: ${detail}`, undefined, false, details)
|
|
27
46
|
}
|
|
28
47
|
return result
|
|
29
48
|
}
|
|
@@ -375,10 +394,618 @@ const exportCommand: CommandDefinition = {
|
|
|
375
394
|
},
|
|
376
395
|
}
|
|
377
396
|
|
|
397
|
+
// ---- extension controls (buckyos aiworkspace README “Extension controls”): source in the Zone's home folders,
|
|
398
|
+
// built into a Workspace, exported as a bundle / `aiws` PIKG, installed into the caller's library, imported elsewhere.
|
|
399
|
+
// The drivers are buckyos-websdk src/aiws_ext.ts, shared with the Desktop.
|
|
400
|
+
|
|
401
|
+
async function sessionToken(ctx: CommandContext): Promise<string> {
|
|
402
|
+
const session = ctx.session
|
|
403
|
+
if (!session) throw new ToolError('AUTH_REQUIRED', 'authenticated session is required', EXIT_AUTH)
|
|
404
|
+
return (await session.ensureValid()).token
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** The Zone file service (NFSP) with the caller's session: where control sources live. */
|
|
408
|
+
async function openHome(ctx: CommandContext): Promise<NfspClient> {
|
|
409
|
+
const session = ctx.session
|
|
410
|
+
if (!session) throw new ToolError('AUTH_REQUIRED', 'authenticated session is required', EXIT_AUTH)
|
|
411
|
+
const nfsp = new NfspClient({ baseUrl: gatewayOrigin(ctx.connection.endpoint), sessionToken: async () => (await session.ensureValid()).token })
|
|
412
|
+
await nfsp.hello(['cli'])
|
|
413
|
+
return nfsp
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** The two calls the shared drivers make (`AiwsExtRpc` of buckyos-websdk src/aiws_ext.ts). */
|
|
417
|
+
function extRpc(ctx: CommandContext) {
|
|
418
|
+
return {
|
|
419
|
+
extBuild: (workspace_id: string, params: Record<string, unknown>) => aiws(ctx, 'ext.build', { workspace_id, ...params }) as never,
|
|
420
|
+
extExport: (workspace_id: string, entity_ids: string[], purpose: string = 'package') => aiws(ctx, 'ext.export', { workspace_id, entity_ids, purpose }) as never,
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Bytes of a Workspace object (`GET /kapi/aiworkspace/asset/<ws>/<obj_id>`, authorized through what references it). */
|
|
425
|
+
function assetFetcher(ctx: CommandContext): (workspaceId: string, objId: string) => Promise<Uint8Array> {
|
|
426
|
+
return async (workspaceId, objId) => {
|
|
427
|
+
const response = await fetch(`${gatewayOrigin(ctx.connection.endpoint)}/kapi/aiworkspace/asset/${encodeURIComponent(workspaceId)}/${encodeURIComponent(objId)}`, {
|
|
428
|
+
headers: { Authorization: `Bearer ${await sessionToken(ctx)}` },
|
|
429
|
+
signal: ctx.signal,
|
|
430
|
+
})
|
|
431
|
+
if (!response.ok) throw new ToolError('AIWS_ASSET_UNAVAILABLE', `asset ${objId}: HTTP ${response.status}`)
|
|
432
|
+
return new Uint8Array(await response.arrayBuffer())
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Source problems (not refusals of the service) as a build failure with diagnostics. */
|
|
437
|
+
async function sourceErrors<T>(run: () => Promise<T>): Promise<T> {
|
|
438
|
+
try {
|
|
439
|
+
return await run()
|
|
440
|
+
} catch (error) {
|
|
441
|
+
if (error instanceof AiwsSourceError) throw new ToolError('BUILD_FAILED', `BUILD_FAILED: ${error.message}`, undefined, false, { data: { diagnostics: error.diagnostics } })
|
|
442
|
+
throw error
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function list(input: Record<string, unknown>, key: string): string[] {
|
|
447
|
+
return requiredInputString(input, key).split(',').map((s) => s.trim()).filter(Boolean)
|
|
448
|
+
}
|
|
449
|
+
|
|
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)
|
|
474
|
+
|
|
475
|
+
const LOCAL_STATE = '.aiws-build.json'
|
|
476
|
+
|
|
477
|
+
async function isLocalDir(path: string): Promise<boolean> {
|
|
478
|
+
try { return (await getHost().stat(path)).isDirectory } catch { return false }
|
|
479
|
+
}
|
|
480
|
+
|
|
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 }[]> {
|
|
483
|
+
const host = getHost()
|
|
484
|
+
const files: { name: string; bytes: Uint8Array }[] = []
|
|
485
|
+
const problems: { file: string; severity: 'error'; message: string }[] = []
|
|
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
|
+
}
|
|
496
|
+
}
|
|
497
|
+
await walk(dir, '', 0)
|
|
498
|
+
if (files.length === 0) problems.push({ file: '', severity: 'error', message: `${dir} holds no files` })
|
|
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 } })
|
|
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
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function localState(dir: string): Promise<Record<string, unknown> | null> {
|
|
519
|
+
try { return JSON.parse(await getHost().readTextFile(getHost().path.join(dir, LOCAL_STATE))) as Record<string, unknown> } catch { return null }
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Build a local folder into a Workspace; the base it was built on is kept in its `.aiws-build.json`. */
|
|
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>> {
|
|
524
|
+
const state = await localState(dir)
|
|
525
|
+
const result = await aiws(ctx, 'ext.build', {
|
|
526
|
+
workspace_id: ws, files: await sourceErrors(async () => packBuildFiles(await localFiles(dir), sourceUploader(ctx))),
|
|
527
|
+
...(opts.folderId ? { folder_id: opts.folderId } : {}),
|
|
528
|
+
...(state && state.workspace_id === ws ? { expect: { def_rev: state.def_rev, dir_rev: state.dir_rev } } : {}),
|
|
529
|
+
...(opts.force ? { force: true } : {}), ...(opts.dryRun ? { dry_run: true } : {}), ...(opts.migration ? { migration: opts.migration } : {}),
|
|
530
|
+
})
|
|
531
|
+
if (!opts.dryRun && typeof result.def_entity_id === 'string' && typeof result.def_rev === 'number') {
|
|
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`)
|
|
533
|
+
}
|
|
534
|
+
return result
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const buildCommand: CommandDefinition = {
|
|
538
|
+
verb: 'build',
|
|
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',
|
|
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).',
|
|
541
|
+
positionals: [{ name: 'dir', description: 'Local folder, or Zone path of the control source folder', required: true }],
|
|
542
|
+
options: [
|
|
543
|
+
WORKSPACE_OPTION,
|
|
544
|
+
{ name: 'folder', property: 'folder_id', description: 'Data folder for the definition (default: data/extensions)', type: 'string' },
|
|
545
|
+
{ name: 'watch', property: 'watch', description: 'Rebuild whenever the folder changes', type: 'boolean' },
|
|
546
|
+
{ name: 'dry-run', property: 'dry_run', description: 'Check only: diagnostics, nothing committed', type: 'boolean' },
|
|
547
|
+
{ name: 'force', property: 'force', description: 'Build over whatever the Workspace holds now', type: 'boolean' },
|
|
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' },
|
|
550
|
+
],
|
|
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']),
|
|
552
|
+
outputSchema: OBJECT_OUTPUT,
|
|
553
|
+
resultSchemaVersion: 1,
|
|
554
|
+
access: { mode: 'fixed', level: 'write' },
|
|
555
|
+
asyncMode: 'sync',
|
|
556
|
+
requiresSession: true,
|
|
557
|
+
examples: ['buckyos aiws build "/home/alice/AI Workspace/Demo/controls/kpi-card" --workspace ws_abc', 'buckyos aiws build "/home/alice/AI Workspace/Demo/controls/kpi-card" --workspace ws_abc --watch'],
|
|
558
|
+
handler: async (ctx, input) => {
|
|
559
|
+
const ws = requiredInputString(input, 'workspace_id')
|
|
560
|
+
const dir = requiredInputString(input, 'dir')
|
|
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) } : {}) }
|
|
563
|
+
if (input.zone_path !== true && await isLocalDir(getHost().path.resolve(ctx.cwd, dir))) {
|
|
564
|
+
const local = getHost().path.resolve(ctx.cwd, dir)
|
|
565
|
+
if (input.watch === true) throw new UsageError('AIWS_USAGE', '--watch follows a Zone folder; rebuild a local folder yourself')
|
|
566
|
+
return buildLocal(ctx, ws, local, opts)
|
|
567
|
+
}
|
|
568
|
+
const nfsp = await openHome(ctx)
|
|
569
|
+
if (input.watch !== true) return sourceErrors(() => buildControl(extRpc(ctx), nfsp, ws, dir, opts))
|
|
570
|
+
let builds = 0
|
|
571
|
+
let failures = 0
|
|
572
|
+
await watchControl(nfsp, dir, async () => {
|
|
573
|
+
try {
|
|
574
|
+
const r = await sourceErrors(() => buildControl(extRpc(ctx), nfsp, ws, dir, opts))
|
|
575
|
+
builds++
|
|
576
|
+
await ctx.io.stderr(`built ${r.ident} v${r.version ?? '?'} (${r.unchanged ? 'unchanged' : r.status})\n`)
|
|
577
|
+
} catch (error) {
|
|
578
|
+
failures++
|
|
579
|
+
const diagnostics = error instanceof ToolError ? (error.details.data as { diagnostics?: unknown[] } | undefined)?.diagnostics : undefined
|
|
580
|
+
await ctx.io.stderr(`build failed: ${error instanceof Error ? error.message : String(error)}\n${diagnostics ? `${JSON.stringify(diagnostics, null, 2)}\n` : ''}`)
|
|
581
|
+
}
|
|
582
|
+
}, { signal: ctx.signal })
|
|
583
|
+
return { ok: true, watched: dir, builds, failures }
|
|
584
|
+
},
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const pullCommand: CommandDefinition = {
|
|
588
|
+
verb: 'pull',
|
|
589
|
+
summary: 'Write a control definition’s Workspace snapshot into a folder — a Zone path (default) or a local one (--local) — as the base of the next build',
|
|
590
|
+
positionals: [{ name: 'def', description: 'Definition (buckyos.block-def) id', required: true }],
|
|
591
|
+
options: [WORKSPACE_OPTION, { name: 'out', property: 'out', description: 'Folder to write (created)', type: 'string', required: true }, { name: 'local', property: 'local', description: 'Write into a local folder instead of the Zone', type: 'boolean' }],
|
|
592
|
+
inputSchema: workspaceInput({ def: { type: 'string', minLength: 1 }, out: { type: 'string', minLength: 1 }, local: { type: 'boolean' } }, ['def', 'out']),
|
|
593
|
+
outputSchema: OBJECT_OUTPUT,
|
|
594
|
+
resultSchemaVersion: 1,
|
|
595
|
+
access: { mode: 'fixed', level: 'write' },
|
|
596
|
+
asyncMode: 'sync',
|
|
597
|
+
requiresSession: true,
|
|
598
|
+
examples: ['buckyos aiws pull def_abc --workspace ws_abc --out "/home/alice/AI Workspace/Demo/controls/kpi-card"'],
|
|
599
|
+
handler: async (ctx, input) => {
|
|
600
|
+
if (input.local === true) {
|
|
601
|
+
const ws = requiredInputString(input, 'workspace_id')
|
|
602
|
+
const host = getHost()
|
|
603
|
+
const out = host.path.resolve(ctx.cwd, requiredInputString(input, 'out'))
|
|
604
|
+
const r = await aiws(ctx, 'ext.export', { workspace_id: ws, entity_ids: [requiredInputString(input, 'def')], purpose: 'pull' })
|
|
605
|
+
const control = expectObject((r.controls as unknown[])[0], 'ext.export.controls[0]')
|
|
606
|
+
await host.mkdir(out, { recursive: true })
|
|
607
|
+
const fetchAsset = assetFetcher(ctx)
|
|
608
|
+
const files: string[] = []
|
|
609
|
+
for (const f of control.files as { name: string; obj_id?: string; text?: string }[]) {
|
|
610
|
+
const target = host.path.join(out, ...f.name.split('/'))
|
|
611
|
+
await host.mkdir(host.path.dirname(target), { recursive: true })
|
|
612
|
+
if (f.text !== undefined) await host.writeTextFile(target, f.text)
|
|
613
|
+
else await host.writeFile(target, await fetchAsset(ws, String(f.obj_id)))
|
|
614
|
+
files.push(f.name)
|
|
615
|
+
}
|
|
616
|
+
await host.writeTextFile(host.path.join(out, LOCAL_STATE), `${JSON.stringify({ workspace_id: ws, def_entity_id: control.entity_id, def_rev: control.def_rev, dir_rev: control.dir_rev, ident: control.ident, at: new Date().toISOString() }, null, 2)}\n`)
|
|
617
|
+
return { ok: true, out, files }
|
|
618
|
+
}
|
|
619
|
+
const nfsp = await openHome(ctx)
|
|
620
|
+
const r = await pullControl(extRpc(ctx), nfsp, requiredInputString(input, 'workspace_id'), requiredInputString(input, 'def'), requiredInputString(input, 'out'), assetFetcher(ctx))
|
|
621
|
+
return { ok: true, out: requiredInputString(input, 'out'), ...r }
|
|
622
|
+
},
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** Run a verb of the pikg module (init / build / pack) with this command's context. */
|
|
626
|
+
async function pikgVerb(ctx: CommandContext, verb: string, input: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
627
|
+
const command = createPikgModule().commands.find((c) => c.verb === verb)
|
|
628
|
+
if (!command) throw new ToolError('INTERNAL', `pikg ${verb} is missing`)
|
|
629
|
+
return expectObject(await command.handler(ctx, input), `pikg ${verb}`)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** Bind a local `.pikg` into the caller's own `user/aiworkspace/<user>/exports` bucket (the data plane PUT, ≤ 32 MiB):
|
|
633
|
+
* what `ext.install { pikg: { bucket_id, key } }` takes (and consumes once installed). */
|
|
634
|
+
async function uploadPackage(ctx: CommandContext, path: string): Promise<{ bucket_id: string; key: string }> {
|
|
635
|
+
const bucket_id = `user/aiworkspace/${ctx.principal.id}/exports`
|
|
636
|
+
const key = `aiws/${Date.now()}-${getHost().path.basename(path)}`
|
|
637
|
+
const bytes = await getHost().readFile(path)
|
|
638
|
+
if (bytes.byteLength > 32 * 1024 * 1024) throw new UsageError('FILE_TOO_LARGE', 'a control package installed from the CLI is limited to 32 MiB')
|
|
639
|
+
const url = `${gatewayOrigin(ctx.connection.endpoint)}/bucket/v1/${bucket_id}/${key.split('/').map(encodeURIComponent).join('/')}`
|
|
640
|
+
const response = await fetch(url, { method: 'PUT', headers: { Authorization: `Bearer ${await sessionToken(ctx)}` }, body: new Blob([bytes as Uint8Array<ArrayBuffer>]), signal: ctx.signal })
|
|
641
|
+
if (!response.ok) throw new ToolError('AIWS_UPLOAD_FAILED', `upload of ${path}: HTTP ${response.status} ${await response.text().catch(() => '')}`)
|
|
642
|
+
return { bucket_id, key }
|
|
643
|
+
}
|
|
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
|
+
|
|
703
|
+
const extExportCommand: CommandDefinition = {
|
|
704
|
+
verb: 'ext-export',
|
|
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',
|
|
706
|
+
description: 'Writes <out>/aiws/ (aiws-manifest.json + controls/<dir>/). --pikg (or --install) also runs pikg init/build/pack in <out> (kind aiws-controls); --install binds the .pikg into your aiworkspace exports bucket and calls ext.install.',
|
|
707
|
+
positionals: [{ name: 'defs', description: 'Definition (or instance) ids, comma-separated', required: true }],
|
|
708
|
+
options: [
|
|
709
|
+
WORKSPACE_OPTION,
|
|
710
|
+
{ name: 'out', property: 'out', description: 'Local folder to write into (created)', type: 'string', required: true },
|
|
711
|
+
...packageOptions('the first control’s'),
|
|
712
|
+
],
|
|
713
|
+
inputSchema: workspaceInput({ defs: { type: 'string', minLength: 1 }, out: { type: 'string', minLength: 1 }, ...PACKAGE_INPUT }, ['defs', 'out']),
|
|
714
|
+
outputSchema: OBJECT_OUTPUT,
|
|
715
|
+
resultSchemaVersion: 1,
|
|
716
|
+
access: { mode: 'fixed', level: 'write' },
|
|
717
|
+
asyncMode: 'sync',
|
|
718
|
+
requiresSession: true,
|
|
719
|
+
examples: ['buckyos aiws ext-export def_abc,def_xyz --workspace ws_abc --out ./kpi-pack --install'],
|
|
720
|
+
handler: async (ctx, input) => {
|
|
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
|
+
})
|
|
757
|
+
},
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const libraryCommand: CommandDefinition = {
|
|
761
|
+
verb: 'library',
|
|
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 },
|
|
769
|
+
outputSchema: OBJECT_OUTPUT,
|
|
770
|
+
resultSchemaVersion: 1,
|
|
771
|
+
access: { mode: 'fixed', level: 'write' },
|
|
772
|
+
asyncMode: 'sync',
|
|
773
|
+
requiresSession: true,
|
|
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'],
|
|
775
|
+
handler: async (ctx, input) => {
|
|
776
|
+
const action = requiredInputString(input, 'action')
|
|
777
|
+
if (action === 'list') return aiws(ctx, 'ext.list_library', {})
|
|
778
|
+
if (action === 'remove') return aiws(ctx, 'ext.uninstall', { pkg_id: requiredInputString(input, 'target') })
|
|
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>')
|
|
789
|
+
const path = getHost().path.resolve(ctx.cwd, requiredInputString(input, 'target'))
|
|
790
|
+
return aiws(ctx, 'ext.install', { pikg: await uploadPackage(ctx, path), mode: 'dev' })
|
|
791
|
+
},
|
|
792
|
+
}
|
|
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
|
+
|
|
823
|
+
const extImportCommand: CommandDefinition = {
|
|
824
|
+
verb: 'ext-import',
|
|
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' } }),
|
|
836
|
+
outputSchema: OBJECT_OUTPUT,
|
|
837
|
+
resultSchemaVersion: 1,
|
|
838
|
+
access: { mode: 'fixed', level: 'write' },
|
|
839
|
+
asyncMode: 'sync',
|
|
840
|
+
requiresSession: true,
|
|
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')
|
|
844
|
+
const folder = inputString(input, 'folder_id')
|
|
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 } : {}) })
|
|
1002
|
+
},
|
|
1003
|
+
}
|
|
1004
|
+
|
|
378
1005
|
export function createAiwsModule(): CommandModule {
|
|
379
1006
|
return {
|
|
380
1007
|
name: 'aiws',
|
|
381
|
-
summary: 'AI Workspace: read a workspace, Block-controls (catalogue, import, export), and execute wish runs as their executor (claim, heartbeat, report)',
|
|
382
|
-
commands: [infoCommand, readCommand, queryCommand, controlsCommand, importCommand, exportCommand, 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],
|
|
383
1010
|
}
|
|
384
1011
|
}
|