@frontera-sdk/cli 0.1.0
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/LICENSE +202 -0
- package/README.md +65 -0
- package/package.json +47 -0
- package/src/api/apps-api.ts +165 -0
- package/src/api/automation-api.ts +140 -0
- package/src/api/platform-api.ts +193 -0
- package/src/api/registry-api.ts +43 -0
- package/src/args.ts +108 -0
- package/src/commands/agent/compose.ts +155 -0
- package/src/commands/agent/index-commands.ts +348 -0
- package/src/commands/agent/resolve.ts +58 -0
- package/src/commands/app/add.ts +78 -0
- package/src/commands/app/deploy.ts +105 -0
- package/src/commands/app/init.ts +53 -0
- package/src/commands/app/list.ts +51 -0
- package/src/commands/app/promote.ts +31 -0
- package/src/commands/app/pull.ts +145 -0
- package/src/commands/app/save.ts +36 -0
- package/src/commands/app/shared.ts +25 -0
- package/src/commands/app/versions.ts +38 -0
- package/src/commands/automation/index-commands.ts +325 -0
- package/src/commands/blueprint/get.ts +160 -0
- package/src/commands/blueprint/list.ts +48 -0
- package/src/commands/blueprint/reserved.ts +40 -0
- package/src/commands/completion.ts +293 -0
- package/src/commands/init.ts +33 -0
- package/src/commands/knowledge/index-commands.ts +140 -0
- package/src/commands/login.ts +103 -0
- package/src/commands/plugin/index-commands.ts +112 -0
- package/src/commands/registry.ts +405 -0
- package/src/commands/skill/index-commands.ts +140 -0
- package/src/commands/types.ts +76 -0
- package/src/config.ts +142 -0
- package/src/context.ts +67 -0
- package/src/errors.ts +30 -0
- package/src/exit.ts +98 -0
- package/src/flag-help.ts +70 -0
- package/src/harness.ts +162 -0
- package/src/heal.ts +418 -0
- package/src/help.ts +128 -0
- package/src/main.ts +204 -0
- package/src/manifest.ts +80 -0
- package/src/output.ts +65 -0
- package/src/pack.ts +18 -0
- package/src/packaging.ts +116 -0
- package/src/project.ts +151 -0
- package/src/prompt.ts +48 -0
- package/src/registry.ts +62 -0
- package/src/secrets.ts +69 -0
- package/src/table.ts +47 -0
- package/src/tar.ts +73 -0
- package/src/template.ts +566 -0
- package/src/vendor/sdk-sources.json +25 -0
package/src/registry.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { RegistryItem } from './api/registry-api'
|
|
5
|
+
|
|
6
|
+
export type { RegistryFile, RegistryItem } from './api/registry-api'
|
|
7
|
+
|
|
8
|
+
export interface WriteResult {
|
|
9
|
+
written: string[]
|
|
10
|
+
skipped: string[]
|
|
11
|
+
npmDependencies: string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Write resolved items into a project.
|
|
16
|
+
*
|
|
17
|
+
* Existing files are SKIPPED unless `force` — components are owned source once
|
|
18
|
+
* copied, so an author who has edited `button.tsx` must not lose that work to
|
|
19
|
+
* an unrelated `frontera app add card` that happens to pull button along as a
|
|
20
|
+
* dependency. Refreshing on purpose is `--force`.
|
|
21
|
+
*/
|
|
22
|
+
export function writeRegistryItems(
|
|
23
|
+
projectDir: string,
|
|
24
|
+
items: RegistryItem[],
|
|
25
|
+
opts: { force?: boolean; srcDir?: string } = {},
|
|
26
|
+
): WriteResult {
|
|
27
|
+
const srcDir = opts.srcDir ?? 'src'
|
|
28
|
+
const written: string[] = []
|
|
29
|
+
const skipped: string[] = []
|
|
30
|
+
const npmDependencies = new Set<string>()
|
|
31
|
+
|
|
32
|
+
for (const item of items) {
|
|
33
|
+
for (const dep of item.dependencies) npmDependencies.add(dep)
|
|
34
|
+
for (const file of item.files) {
|
|
35
|
+
const dest = join(projectDir, srcDir, file.path)
|
|
36
|
+
if (existsSync(dest) && !opts.force) {
|
|
37
|
+
skipped.push(file.path)
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
mkdirSync(dirname(dest), { recursive: true })
|
|
41
|
+
writeFileSync(dest, file.content)
|
|
42
|
+
written.push(file.path)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { written, skipped, npmDependencies: [...npmDependencies] }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** npm dependencies an item needs that the project does not already declare. */
|
|
50
|
+
export function missingDependencies(projectDir: string, required: string[]): string[] {
|
|
51
|
+
const pkgPath = join(projectDir, 'package.json')
|
|
52
|
+
if (!existsSync(pkgPath)) return required
|
|
53
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as {
|
|
54
|
+
dependencies?: Record<string, string>
|
|
55
|
+
devDependencies?: Record<string, string>
|
|
56
|
+
}
|
|
57
|
+
const declared = new Set([
|
|
58
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
59
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
60
|
+
])
|
|
61
|
+
return required.filter((d) => !declared.has(d))
|
|
62
|
+
}
|
package/src/secrets.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
import { UsageError } from './errors'
|
|
4
|
+
|
|
5
|
+
export interface SecretSpec {
|
|
6
|
+
/** `literal` — the value itself; `ref` — the name of a workspace secret. */
|
|
7
|
+
kind: 'literal' | 'ref'
|
|
8
|
+
value: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* How a secret enters the CLI.
|
|
13
|
+
*
|
|
14
|
+
* Never as an inline flag value. `--api-key hunter2` lands in process
|
|
15
|
+
* listings and shell history, and a CLI that accepts it will have it used, so
|
|
16
|
+
* the raw form is refused rather than merely discouraged.
|
|
17
|
+
*
|
|
18
|
+
* name@- read from stdin
|
|
19
|
+
* name@./file read from a file
|
|
20
|
+
* name=ref:my-key reference an existing workspace secret
|
|
21
|
+
*
|
|
22
|
+
* The reference form is preferred: the value never enters a document, a flag
|
|
23
|
+
* or a shell, and the workspace secret store already keeps a
|
|
24
|
+
* create/update/delete/resolve/rotate audit trail.
|
|
25
|
+
*/
|
|
26
|
+
export async function readSecret(spec: string): Promise<{ name: string; secret: SecretSpec }> {
|
|
27
|
+
const at = spec.indexOf('@')
|
|
28
|
+
const eq = spec.indexOf('=')
|
|
29
|
+
|
|
30
|
+
if (at > 0 && (eq === -1 || at < eq)) {
|
|
31
|
+
const name = spec.slice(0, at)
|
|
32
|
+
const source = spec.slice(at + 1)
|
|
33
|
+
return { name, secret: { kind: 'literal', value: await readSecretValue(source) } }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (eq > 0) {
|
|
37
|
+
const name = spec.slice(0, eq)
|
|
38
|
+
const value = spec.slice(eq + 1)
|
|
39
|
+
if (value.startsWith('ref:')) {
|
|
40
|
+
return { name, secret: { kind: 'ref', value: value.slice('ref:'.length) } }
|
|
41
|
+
}
|
|
42
|
+
throw new UsageError(
|
|
43
|
+
`refusing an inline secret value for "${name}"`,
|
|
44
|
+
`pass it as ${name}@- (stdin), ${name}@./file, or ${name}=ref:<workspace-secret>`,
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new UsageError(
|
|
49
|
+
`could not read a secret from "${spec}"`,
|
|
50
|
+
'use name@- , name@./file , or name=ref:<workspace-secret>',
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Read a secret's bytes from stdin (`-`) or a file path. */
|
|
55
|
+
export async function readSecretValue(source: string): Promise<string> {
|
|
56
|
+
const path = source.startsWith('@') ? source.slice(1) : source
|
|
57
|
+
if (path === '-') return readStdin()
|
|
58
|
+
try {
|
|
59
|
+
return readFileSync(path, 'utf8')
|
|
60
|
+
} catch {
|
|
61
|
+
throw new UsageError(`could not read ${path}`, 'check the path, or pipe the value in with @-')
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function readStdin(): Promise<string> {
|
|
66
|
+
const chunks: Uint8Array[] = []
|
|
67
|
+
for await (const chunk of Bun.stdin.stream()) chunks.push(chunk)
|
|
68
|
+
return Buffer.concat(chunks).toString('utf8')
|
|
69
|
+
}
|
package/src/table.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A plain, aligned table for human output.
|
|
3
|
+
*
|
|
4
|
+
* Every list command prints an identifier the caller is expected to pass to
|
|
5
|
+
* the next command, and without a header there is no way to tell which column
|
|
6
|
+
* that is — `blueprint list` printed the api name beside the display name and
|
|
7
|
+
* a reader could only guess which one `get` wanted.
|
|
8
|
+
*
|
|
9
|
+
* Columns are padded to their content, never truncated: an id cut to fit is an
|
|
10
|
+
* id that cannot be copied, which defeats the point of printing it. Wide rows
|
|
11
|
+
* wrap in the terminal instead, which is recoverable.
|
|
12
|
+
*
|
|
13
|
+
* Human output only. `--json` returns the payload, and anything parsing this
|
|
14
|
+
* text should be using that instead.
|
|
15
|
+
*/
|
|
16
|
+
export function table(
|
|
17
|
+
headers: string[],
|
|
18
|
+
rows: string[][],
|
|
19
|
+
/**
|
|
20
|
+
* Per-column cap, by index. Only for free text.
|
|
21
|
+
*
|
|
22
|
+
* A skill description can run to 600 characters, which wrapped every row
|
|
23
|
+
* and destroyed the alignment the table exists for. Identifiers must never
|
|
24
|
+
* be given a cap — one cut to fit cannot be copied, which defeats printing
|
|
25
|
+
* it — so this is opt-in per column rather than a global width.
|
|
26
|
+
*/
|
|
27
|
+
maxWidths: Array<number | undefined> = [],
|
|
28
|
+
): string {
|
|
29
|
+
if (rows.length === 0) return ''
|
|
30
|
+
|
|
31
|
+
const clip = (value: string, i: number): string => {
|
|
32
|
+
const max = maxWidths[i]
|
|
33
|
+
if (!max || value.length <= max) return value
|
|
34
|
+
return `${value.slice(0, max - 1).trimEnd()}…`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const clipped = rows.map((r) => r.map((cell, i) => clip(cell ?? '', i)))
|
|
38
|
+
const widths = headers.map((h, i) =>
|
|
39
|
+
Math.max(h.length, ...clipped.map((r) => (r[i] ?? '').length)),
|
|
40
|
+
)
|
|
41
|
+
rows = clipped
|
|
42
|
+
|
|
43
|
+
const line = (cells: string[]) =>
|
|
44
|
+
` ${cells.map((c, i) => (i === cells.length - 1 ? c : c.padEnd(widths[i]!))).join(' ').trimEnd()}`
|
|
45
|
+
|
|
46
|
+
return [line(headers.map((h) => h.toUpperCase())), ...rows.map(line)].join('\n')
|
|
47
|
+
}
|
package/src/tar.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { gzipSync } from 'node:zlib'
|
|
2
|
+
|
|
3
|
+
import type { SourceFile } from './packaging'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Write a gzipped USTAR archive.
|
|
7
|
+
*
|
|
8
|
+
* ustar specifically, not GNU: the service's reader parses ustar headers only,
|
|
9
|
+
* and GNU's long-name ('L') entries would be skipped. The service rejects any
|
|
10
|
+
* path over 255 chars rather than dropping it, so an over-long path fails
|
|
11
|
+
* loudly at upload instead of silently vanishing from the bundle.
|
|
12
|
+
*/
|
|
13
|
+
export const MAX_USTAR_PATH = 255
|
|
14
|
+
|
|
15
|
+
export function createTarGz(files: SourceFile[]): Uint8Array {
|
|
16
|
+
const enc = new TextEncoder()
|
|
17
|
+
const blocks: Uint8Array[] = []
|
|
18
|
+
|
|
19
|
+
for (const file of files) {
|
|
20
|
+
if (file.rel.length > MAX_USTAR_PATH) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`path too long for ustar (${file.rel.length} > ${MAX_USTAR_PATH}): ${file.rel}`,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const header = new Uint8Array(512)
|
|
27
|
+
const write = (off: number, value: string, len: number) =>
|
|
28
|
+
header.set(enc.encode(value.slice(0, len)), off)
|
|
29
|
+
|
|
30
|
+
// ustar splits a long path across prefix(155) + name(100).
|
|
31
|
+
let name = file.rel
|
|
32
|
+
let prefix = ''
|
|
33
|
+
if (name.length > 100) {
|
|
34
|
+
const cut = name.lastIndexOf('/', 100)
|
|
35
|
+
if (cut <= 0) throw new Error(`cannot split path for ustar: ${file.rel}`)
|
|
36
|
+
prefix = name.slice(0, cut)
|
|
37
|
+
name = name.slice(cut + 1)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
write(0, name, 100)
|
|
41
|
+
write(100, '0000644', 8) // mode
|
|
42
|
+
write(108, '0000000', 8) // uid
|
|
43
|
+
write(116, '0000000', 8) // gid
|
|
44
|
+
write(124, file.bytes.byteLength.toString(8).padStart(11, '0'), 12)
|
|
45
|
+
write(136, Math.floor(Date.now() / 1000).toString(8).padStart(11, '0'), 12)
|
|
46
|
+
write(148, ' ', 8) // checksum placeholder (spaces during calc)
|
|
47
|
+
header[156] = 0x30 // '0' regular file
|
|
48
|
+
write(257, 'ustar', 6)
|
|
49
|
+
write(263, '00', 2)
|
|
50
|
+
write(345, prefix, 155)
|
|
51
|
+
|
|
52
|
+
// Header checksum: sum of all bytes with the checksum field as spaces.
|
|
53
|
+
let sum = 0
|
|
54
|
+
for (const b of header) sum += b
|
|
55
|
+
write(148, `${sum.toString(8).padStart(6, '0')}\0 `, 8)
|
|
56
|
+
|
|
57
|
+
blocks.push(header)
|
|
58
|
+
const padded = new Uint8Array(Math.ceil(file.bytes.byteLength / 512) * 512)
|
|
59
|
+
padded.set(file.bytes)
|
|
60
|
+
blocks.push(padded)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
blocks.push(new Uint8Array(1024)) // two zero blocks = EOF
|
|
64
|
+
|
|
65
|
+
const total = blocks.reduce((n, b) => n + b.length, 0)
|
|
66
|
+
const tar = new Uint8Array(total)
|
|
67
|
+
let off = 0
|
|
68
|
+
for (const b of blocks) {
|
|
69
|
+
tar.set(b, off)
|
|
70
|
+
off += b.length
|
|
71
|
+
}
|
|
72
|
+
return new Uint8Array(gzipSync(Buffer.from(tar)))
|
|
73
|
+
}
|