@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.
Files changed (53) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +65 -0
  3. package/package.json +47 -0
  4. package/src/api/apps-api.ts +165 -0
  5. package/src/api/automation-api.ts +140 -0
  6. package/src/api/platform-api.ts +193 -0
  7. package/src/api/registry-api.ts +43 -0
  8. package/src/args.ts +108 -0
  9. package/src/commands/agent/compose.ts +155 -0
  10. package/src/commands/agent/index-commands.ts +348 -0
  11. package/src/commands/agent/resolve.ts +58 -0
  12. package/src/commands/app/add.ts +78 -0
  13. package/src/commands/app/deploy.ts +105 -0
  14. package/src/commands/app/init.ts +53 -0
  15. package/src/commands/app/list.ts +51 -0
  16. package/src/commands/app/promote.ts +31 -0
  17. package/src/commands/app/pull.ts +145 -0
  18. package/src/commands/app/save.ts +36 -0
  19. package/src/commands/app/shared.ts +25 -0
  20. package/src/commands/app/versions.ts +38 -0
  21. package/src/commands/automation/index-commands.ts +325 -0
  22. package/src/commands/blueprint/get.ts +160 -0
  23. package/src/commands/blueprint/list.ts +48 -0
  24. package/src/commands/blueprint/reserved.ts +40 -0
  25. package/src/commands/completion.ts +293 -0
  26. package/src/commands/init.ts +33 -0
  27. package/src/commands/knowledge/index-commands.ts +140 -0
  28. package/src/commands/login.ts +103 -0
  29. package/src/commands/plugin/index-commands.ts +112 -0
  30. package/src/commands/registry.ts +405 -0
  31. package/src/commands/skill/index-commands.ts +140 -0
  32. package/src/commands/types.ts +76 -0
  33. package/src/config.ts +142 -0
  34. package/src/context.ts +67 -0
  35. package/src/errors.ts +30 -0
  36. package/src/exit.ts +98 -0
  37. package/src/flag-help.ts +70 -0
  38. package/src/harness.ts +162 -0
  39. package/src/heal.ts +418 -0
  40. package/src/help.ts +128 -0
  41. package/src/main.ts +204 -0
  42. package/src/manifest.ts +80 -0
  43. package/src/output.ts +65 -0
  44. package/src/pack.ts +18 -0
  45. package/src/packaging.ts +116 -0
  46. package/src/project.ts +151 -0
  47. package/src/prompt.ts +48 -0
  48. package/src/registry.ts +62 -0
  49. package/src/secrets.ts +69 -0
  50. package/src/table.ts +47 -0
  51. package/src/tar.ts +73 -0
  52. package/src/template.ts +566 -0
  53. package/src/vendor/sdk-sources.json +25 -0
@@ -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
+ }