@frontera-sdk/cli 1.52.44 → 1.52.45
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/package.json +4 -4
- package/src/commands/kit/vendor.ts +62 -48
- package/src/commands/local/init.ts +22 -2
- package/src/flag-help.ts +1 -0
- package/src/local/local-keys.ts +5 -3
- package/src/local/local-seed.ts +1 -1
- package/src/local/local-studio-server.ts +13 -7
- package/src/vendor/kit-assets.json +66 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontera-sdk/cli",
|
|
3
|
-
"version": "1.52.
|
|
3
|
+
"version": "1.52.45",
|
|
4
4
|
"description": "The frontera CLI — scaffold, pull, save and deploy Frontera apps and automations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontera",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/claude-agent-sdk": "^0.3.251",
|
|
42
42
|
"@faker-js/faker": "10.6.0",
|
|
43
|
-
"@frontera-sdk/core": "1.52.
|
|
44
|
-
"@frontera-sdk/functions": "1.52.
|
|
43
|
+
"@frontera-sdk/core": "1.52.45",
|
|
44
|
+
"@frontera-sdk/functions": "1.52.45",
|
|
45
45
|
"ai": "^6.0.116",
|
|
46
46
|
"axe-core": "4.12.1",
|
|
47
47
|
"gray-matter": "^4.0.3",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"yaml": "^2.9.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@frontera-sdk/forge-contracts": "1.52.
|
|
52
|
+
"@frontera-sdk/forge-contracts": "1.52.45",
|
|
53
53
|
"@types/bun": "^1.3.14",
|
|
54
54
|
"typescript": "^5.9.3"
|
|
55
55
|
}
|
|
@@ -44,55 +44,8 @@ export const kitVendor: Command = {
|
|
|
44
44
|
requireCompatible()
|
|
45
45
|
|
|
46
46
|
const root = kitRoot(ctx)
|
|
47
|
-
const rendered = renderKit()
|
|
48
|
-
const lock = readLock(root)
|
|
49
47
|
const force = flagBool(ctx, 'force')
|
|
50
|
-
|
|
51
|
-
const report = writeGenerated(root, rendered.generated, lock, { force })
|
|
52
|
-
|
|
53
|
-
const edited: string[] = []
|
|
54
|
-
const repaired: string[] = []
|
|
55
|
-
|
|
56
|
-
if (rendered.agentsMd !== null) {
|
|
57
|
-
const path = join(root, 'AGENTS.md')
|
|
58
|
-
const existing = existsSync(path) ? readFileSync(path, 'utf8') : null
|
|
59
|
-
const next = applyManagedBlock(existing, rendered.agentsMd)
|
|
60
|
-
if (next !== existing) {
|
|
61
|
-
writeFileSync(path, next)
|
|
62
|
-
edited.push('AGENTS.md')
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (rendered.claudeMd !== null) {
|
|
67
|
-
const path = join(root, 'CLAUDE.md')
|
|
68
|
-
const existing = existsSync(path) ? readFileSync(path, 'utf8') : null
|
|
69
|
-
const result = applyClaudeImport(existing, rendered.claudeMd)
|
|
70
|
-
if (result.content !== existing) {
|
|
71
|
-
writeFileSync(path, result.content)
|
|
72
|
-
edited.push('CLAUDE.md')
|
|
73
|
-
}
|
|
74
|
-
if (result.repaired) repaired.push('CLAUDE.md')
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// The lock records what WAS written. A conflicted file keeps whatever digest
|
|
78
|
-
// it had, so the next run still reports it rather than adopting the local
|
|
79
|
-
// edit as ours.
|
|
80
|
-
const locked = Object.fromEntries(
|
|
81
|
-
Object.entries(rendered.generated).filter(([rel]) => !report.conflicts.includes(rel)),
|
|
82
|
-
)
|
|
83
|
-
writeLock(root, locked, cliVersion())
|
|
84
|
-
|
|
85
|
-
const data = {
|
|
86
|
-
root,
|
|
87
|
-
kitVersion: KIT.kitVersion,
|
|
88
|
-
hosts: KIT.hosts,
|
|
89
|
-
written: report.written,
|
|
90
|
-
unchanged: report.unchanged,
|
|
91
|
-
conflicts: report.conflicts,
|
|
92
|
-
edited,
|
|
93
|
-
repaired,
|
|
94
|
-
lock: LOCK_FILE,
|
|
95
|
-
}
|
|
48
|
+
const { data, report, edited, repaired } = vendorKit(root, { force })
|
|
96
49
|
|
|
97
50
|
const lines = [
|
|
98
51
|
...report.written.map((f) => ` + ${f}`),
|
|
@@ -117,4 +70,65 @@ export const kitVendor: Command = {
|
|
|
117
70
|
},
|
|
118
71
|
}
|
|
119
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Write the kit into `root`: the skills in both host directories, the managed
|
|
75
|
+
* AGENTS.md block, the CLAUDE.md import and the lock.
|
|
76
|
+
*
|
|
77
|
+
* Shared with `frontera local init`, so a project folder Forge opens carries the
|
|
78
|
+
* whole kit, the solution skills included, rather than only the CLI skill.
|
|
79
|
+
*/
|
|
80
|
+
export function vendorKit(root: string, opts: { force?: boolean } = {}) {
|
|
81
|
+
const rendered = renderKit()
|
|
82
|
+
const lock = readLock(root)
|
|
83
|
+
const force = opts.force === true
|
|
84
|
+
|
|
85
|
+
const report = writeGenerated(root, rendered.generated, lock, { force })
|
|
86
|
+
|
|
87
|
+
const edited: string[] = []
|
|
88
|
+
const repaired: string[] = []
|
|
89
|
+
|
|
90
|
+
if (rendered.agentsMd !== null) {
|
|
91
|
+
const path = join(root, 'AGENTS.md')
|
|
92
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf8') : null
|
|
93
|
+
const next = applyManagedBlock(existing, rendered.agentsMd)
|
|
94
|
+
if (next !== existing) {
|
|
95
|
+
writeFileSync(path, next)
|
|
96
|
+
edited.push('AGENTS.md')
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (rendered.claudeMd !== null) {
|
|
101
|
+
const path = join(root, 'CLAUDE.md')
|
|
102
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf8') : null
|
|
103
|
+
const result = applyClaudeImport(existing, rendered.claudeMd)
|
|
104
|
+
if (result.content !== existing) {
|
|
105
|
+
writeFileSync(path, result.content)
|
|
106
|
+
edited.push('CLAUDE.md')
|
|
107
|
+
}
|
|
108
|
+
if (result.repaired) repaired.push('CLAUDE.md')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The lock records what WAS written. A conflicted file keeps whatever digest
|
|
112
|
+
// it had, so the next run still reports it rather than adopting the local
|
|
113
|
+
// edit as ours.
|
|
114
|
+
const locked = Object.fromEntries(
|
|
115
|
+
Object.entries(rendered.generated).filter(([rel]) => !report.conflicts.includes(rel)),
|
|
116
|
+
)
|
|
117
|
+
writeLock(root, locked, cliVersion())
|
|
118
|
+
|
|
119
|
+
const data = {
|
|
120
|
+
root,
|
|
121
|
+
kitVersion: KIT.kitVersion,
|
|
122
|
+
hosts: KIT.hosts,
|
|
123
|
+
written: report.written,
|
|
124
|
+
unchanged: report.unchanged,
|
|
125
|
+
conflicts: report.conflicts,
|
|
126
|
+
edited,
|
|
127
|
+
repaired,
|
|
128
|
+
lock: LOCK_FILE,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { data, report, edited, repaired }
|
|
132
|
+
}
|
|
133
|
+
|
|
120
134
|
export { CliError }
|
|
@@ -10,6 +10,9 @@ import { readLink } from '../../local/local-sync-link'
|
|
|
10
10
|
import { generateSyntheticData, addMissingBackings, loadScenario, SCENARIO_TEMPLATE } from '../../local/local-synthetic-data'
|
|
11
11
|
import { isoDay, writeSampleOptions } from '../../local/local-sample-remake'
|
|
12
12
|
import { migrateMcpConfigs, writeSolutionScaffold } from '../../local/local-scaffold'
|
|
13
|
+
import { checkCompatibility } from '../../kit'
|
|
14
|
+
import { cliVersion } from '../kit/shared'
|
|
15
|
+
import { vendorKit } from '../kit/vendor'
|
|
13
16
|
import { flagBool, flagString, type Command, type CommandContext } from '../types'
|
|
14
17
|
import { localSync } from './sync'
|
|
15
18
|
|
|
@@ -39,6 +42,7 @@ export const localInit: Command = {
|
|
|
39
42
|
kinds: 'string',
|
|
40
43
|
'no-sample-data': 'boolean',
|
|
41
44
|
'no-app-code': 'boolean',
|
|
45
|
+
'no-kit': 'boolean',
|
|
42
46
|
rows: 'string',
|
|
43
47
|
seed: 'string',
|
|
44
48
|
},
|
|
@@ -80,9 +84,25 @@ export const localInit: Command = {
|
|
|
80
84
|
const at = scaffold.skipped.indexOf(file)
|
|
81
85
|
if (at >= 0) scaffold.skipped.splice(at, 1)
|
|
82
86
|
}
|
|
87
|
+
// The whole authoring kit, after the scaffold so its managed block lands in
|
|
88
|
+
// the project's AGENTS.md: the solution skills (delivery, Agents, workflows,
|
|
89
|
+
// Functions, Apps, embeds, review) reach every coding agent working here,
|
|
90
|
+
// not only the CLI skill. `--no-kit` leaves it out, for a machine that uses
|
|
91
|
+
// the installed plugin instead (`frontera kit doctor` finds duplicates).
|
|
92
|
+
const kit = { written: [] as string[], skipped: [] as string[] }
|
|
93
|
+
if (!flagBool(ctx, 'no-kit')) {
|
|
94
|
+
const compat = checkCompatibility(cliVersion())
|
|
95
|
+
if (compat.compatible) {
|
|
96
|
+
const vendored = vendorKit(target, { force })
|
|
97
|
+
kit.written.push(...vendored.report.written, ...vendored.edited)
|
|
98
|
+
kit.skipped.push(...vendored.report.conflicts)
|
|
99
|
+
} else {
|
|
100
|
+
ctx.output.note(` the Frontera skills were not added: ${compat.reason}; run \`frontera kit vendor\` after updating the CLI`)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
83
103
|
const listed = (files: string[]) => files.filter((f) => !f.startsWith('vendor/'))
|
|
84
|
-
const written = [...new Set(listed([...scaffold.written, ...harness.written]))]
|
|
85
|
-
const skipped = listed([...scaffold.skipped, ...harness.skipped]).filter((f) => !written.includes(f))
|
|
104
|
+
const written = [...new Set(listed([...scaffold.written, ...harness.written, ...kit.written]))]
|
|
105
|
+
const skipped = listed([...scaffold.skipped, ...harness.skipped, ...kit.skipped]).filter((f) => !written.includes(f))
|
|
86
106
|
|
|
87
107
|
let installed: boolean | null = null
|
|
88
108
|
if (!flagBool(ctx, 'no-install') && scaffold.written.includes('package.json')) {
|
package/src/flag-help.ts
CHANGED
|
@@ -276,6 +276,7 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
276
276
|
conflicts: 'the conflicts the picks were chosen against, as `diff --json` names them; refused if they moved',
|
|
277
277
|
'from-workspace': 'start the project from a platform workspace instead of the examples',
|
|
278
278
|
'no-sample-data': 'do not generate sample rows for the pulled Blueprint',
|
|
279
|
+
'no-kit': 'leave out the Frontera skills (the kit) that local init vendors into the project; for a machine that uses the installed plugin instead',
|
|
279
280
|
'keep-sample-data': 'leave the sample datasets as they are; by default they are made again (same seed, rows and day) after a pull that brings Blueprint items, or when an older generator made them',
|
|
280
281
|
'remake-sample-data': 'make the sample datasets again with the newest sample rules (same seed, rows and day), even the ones a pull wrote, which local dev otherwise keeps as pulled',
|
|
281
282
|
synthetic: 'generate sample rows for the Blueprint: values fit each property, links stay consistent',
|
package/src/local/local-keys.ts
CHANGED
|
@@ -21,8 +21,8 @@ const SERVICE = 'frontera-local-model-key'
|
|
|
21
21
|
export interface ProviderSpec {
|
|
22
22
|
id: string
|
|
23
23
|
label: string
|
|
24
|
-
/** The variable the service reads for this provider. */
|
|
25
|
-
envVar
|
|
24
|
+
/** The variable the service reads for this provider; none for a key only an organization holds. */
|
|
25
|
+
envVar?: string
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/** Providers the local runtime can pass a key to; mirrors the service's built-in providers. */
|
|
@@ -32,6 +32,8 @@ export const LOCAL_PROVIDERS: readonly ProviderSpec[] = [
|
|
|
32
32
|
{ id: 'openrouter', label: 'OpenRouter', envVar: 'OPENROUTER_API_KEY' },
|
|
33
33
|
{ id: 'google', label: 'Google Gemini', envVar: 'GOOGLE_GENERATIVE_AI_API_KEY' },
|
|
34
34
|
{ id: 'cohere', label: 'Cohere (reranking)', envVar: 'COHERE_API_KEY' },
|
|
35
|
+
// Agents' web search; stored as the local organization's own key, never an environment variable.
|
|
36
|
+
{ id: 'openrouter-web-search', label: 'OpenRouter web search' },
|
|
35
37
|
]
|
|
36
38
|
|
|
37
39
|
export function findProvider(id: string): ProviderSpec | undefined {
|
|
@@ -100,7 +102,7 @@ export function providerEnv(): Record<string, string> {
|
|
|
100
102
|
const env: Record<string, string> = {}
|
|
101
103
|
for (const p of LOCAL_PROVIDERS) {
|
|
102
104
|
const key = getKey(p.id)
|
|
103
|
-
if (key) env[p.envVar] = key
|
|
105
|
+
if (key && p.envVar) env[p.envVar] = key
|
|
104
106
|
}
|
|
105
107
|
return env
|
|
106
108
|
}
|
package/src/local/local-seed.ts
CHANGED
|
@@ -166,7 +166,7 @@ export async function seedLocalRuntime(args: SeedArgs): Promise<LocalCredentials
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/** Provider ids the service stores as organization credentials, each with an `apiKey` field. */
|
|
169
|
-
const CREDENTIAL_PROVIDERS = new Set(['anthropic', 'openai', 'openrouter', 'google'])
|
|
169
|
+
const CREDENTIAL_PROVIDERS = new Set(['anthropic', 'openai', 'openrouter', 'google', 'openrouter-web-search'])
|
|
170
170
|
|
|
171
171
|
/** Embedding defaults by provider, in preference order; the service requires 1536-wide vectors. */
|
|
172
172
|
const EMBEDDING: Array<{ provider: string; model: string }> = [
|
|
@@ -386,14 +386,20 @@ export function startStudioServer(opts: StudioServerOptions) {
|
|
|
386
386
|
if (state?.credentials?.workspaceId && !headers.has('x-workspace-id')) {
|
|
387
387
|
headers.set('x-workspace-id', state.credentials.workspaceId)
|
|
388
388
|
}
|
|
389
|
+
// A page sends the session token it kept (`bearer_token`) itself. After
|
|
390
|
+
// `frontera local reset` that session is gone, so a session the service
|
|
391
|
+
// refuses is answered as the owner; an API key or a signed token is not.
|
|
392
|
+
const kept = headers.get('authorization')?.match(/^Bearer ([A-Za-z0-9]{32})$/)?.[1]
|
|
393
|
+
const fallback = kept && bearer && kept !== bearer ? bearer : null
|
|
394
|
+
const hasBody = req.method !== 'GET' && req.method !== 'HEAD'
|
|
395
|
+
const body = hasBody && fallback ? await req.arrayBuffer() : hasBody ? req.body : undefined
|
|
389
396
|
try {
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
})
|
|
397
|
+
const send = () => fetch(target, { method: req.method, headers, body, redirect: 'manual', duplex: 'half' })
|
|
398
|
+
let res = await send()
|
|
399
|
+
if (res.status === 401 && fallback) {
|
|
400
|
+
headers.set('authorization', `Bearer ${fallback}`)
|
|
401
|
+
res = await send()
|
|
402
|
+
}
|
|
397
403
|
const out = new Headers(res.headers)
|
|
398
404
|
out.delete('content-encoding')
|
|
399
405
|
out.delete('content-length')
|