@frontera-sdk/cli 1.52.39 → 1.52.41
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/README.md +9 -0
- package/package.json +6 -4
- package/src/adopt.ts +6 -2
- package/src/api/apps-api.ts +86 -4
- package/src/api/blueprint-authoring-api.ts +35 -11
- package/src/api/dataset-api.ts +29 -0
- package/src/api/governed-action-api.ts +90 -2
- package/src/api/platform-api.ts +87 -2
- package/src/api/workflow-api.ts +33 -2
- package/src/app-contract.ts +156 -0
- package/src/app-lock.ts +125 -0
- package/src/args.ts +14 -3
- package/src/auth-verify.ts +10 -0
- package/src/automation-template.ts +12 -1
- package/src/blueprint/action-rules.ts +516 -0
- package/src/blueprint/action-semantic-fit.ts +205 -0
- package/src/blueprint/compile.ts +51 -2
- package/src/blueprint/diff.ts +34 -2
- package/src/blueprint/metric-query.ts +215 -0
- package/src/blueprint/metric-rules.ts +122 -0
- package/src/blueprint/object-sets.ts +116 -0
- package/src/blueprint/ontology-edit-plan.ts +49 -5
- package/src/blueprint/projection.ts +24 -1
- package/src/blueprint/scaffold.ts +84 -42
- package/src/commands/action/available.ts +1 -1
- package/src/commands/action/criteria-refusal.ts +84 -0
- package/src/commands/action/decide.ts +5 -2
- package/src/commands/action/deploy.ts +13 -4
- package/src/commands/action/list.ts +134 -19
- package/src/commands/action/requests.ts +34 -6
- package/src/commands/action/submit.ts +28 -13
- package/src/commands/agent/draft-changes.ts +1 -1
- package/src/commands/agent/index-commands.ts +69 -7
- package/src/commands/agent/resolve.ts +2 -0
- package/src/commands/app/checks.ts +44 -0
- package/src/commands/app/deploy.ts +243 -60
- package/src/commands/app/deployments.ts +97 -0
- package/src/commands/app/dev.ts +103 -11
- package/src/commands/app/init.ts +36 -3
- package/src/commands/app/promote.ts +45 -19
- package/src/commands/app/pull.ts +3 -1
- package/src/commands/app/rollback.ts +46 -0
- package/src/commands/app/sdk.ts +54 -8
- package/src/commands/app/unpin.ts +27 -0
- package/src/commands/app/versions.ts +1 -1
- package/src/commands/automation/bindings.ts +4 -2
- package/src/commands/automation/build-entry.ts +4 -1
- package/src/commands/automation/index-commands.ts +3 -1
- package/src/commands/automation/pull.ts +6 -1
- package/src/commands/automation/run.ts +34 -9
- package/src/commands/blueprint/bind.ts +2 -1
- package/src/commands/blueprint/declarative.ts +332 -32
- package/src/commands/blueprint/editable.ts +3 -3
- package/src/commands/blueprint/get.ts +6 -2
- package/src/commands/blueprint/grants.ts +64 -10
- package/src/commands/blueprint/metric.ts +108 -0
- package/src/commands/blueprint/object-sets.ts +71 -0
- package/src/commands/blueprint/query.ts +155 -31
- package/src/commands/dataset/index-commands.ts +156 -7
- package/src/commands/knowledge/index-commands.ts +122 -20
- package/src/commands/local/check.ts +657 -44
- package/src/commands/local/data.ts +33 -12
- package/src/commands/local/dev.ts +876 -89
- package/src/commands/local/docs-mcp.ts +10 -18
- package/src/commands/local/index-commands.ts +300 -63
- package/src/commands/local/init.ts +18 -5
- package/src/commands/local/key.ts +0 -3
- package/src/commands/local/providers.ts +0 -7
- package/src/commands/local/remove.ts +116 -0
- package/src/commands/local/schema.ts +3 -4
- package/src/commands/local/sdk-refresh.ts +59 -0
- package/src/commands/local/snapshot.ts +100 -0
- package/src/commands/local/sync-request.ts +321 -14
- package/src/commands/local/sync.ts +103 -6
- package/src/commands/mcp/serve.ts +34 -0
- package/src/commands/registry.ts +54 -10
- package/src/commands/types.ts +29 -0
- package/src/commands/workflow/archive.ts +46 -0
- package/src/commands/workflow/get.ts +2 -1
- package/src/commands/workflow/index-commands.ts +5 -0
- package/src/commands/workflow/push.ts +49 -2
- package/src/commands/workflow/run.ts +65 -7
- package/src/commands/workflow/runs.ts +9 -6
- package/src/commands/workflow/webhook-test.ts +166 -0
- package/src/config.ts +43 -6
- package/src/dev-broker.ts +52 -0
- package/src/exit.ts +103 -4
- package/src/flag-help.ts +77 -17
- package/src/forge/claude-commands.ts +11 -6
- package/src/forge/claude-skills.ts +4 -4
- package/src/forge/touches.ts +15 -12
- package/src/function-outside-imports.ts +89 -0
- package/src/harness.ts +42 -24
- package/src/heal.ts +113 -2
- package/src/kit.ts +1 -1
- package/src/local/fix-words.ts +12 -0
- package/src/local/function-bundle-recovery.ts +775 -0
- package/src/local/local-actions.ts +103 -10
- package/src/local/local-agent-action-grants.ts +210 -0
- package/src/local/local-agent-checks.ts +232 -0
- package/src/local/local-agent-grants.ts +29 -7
- package/src/local/local-agent-lean-plugins.ts +68 -0
- package/src/local/local-agent-missing-tools.ts +68 -0
- package/src/local/local-agent-name.ts +27 -0
- package/src/local/local-app-code.ts +191 -0
- package/src/local/local-app-dev-files.ts +186 -0
- package/src/local/local-app-dev-session.ts +88 -0
- package/src/local/local-app-lock-install.ts +114 -0
- package/src/local/local-app-pulled.ts +167 -0
- package/src/local/local-app-restore.ts +111 -0
- package/src/local/local-app-runtime.ts +68 -0
- package/src/local/local-app-sdk-paths.ts +149 -0
- package/src/local/local-app-sdk.ts +509 -0
- package/src/local/local-applied.ts +212 -22
- package/src/local/local-apply-history.ts +84 -0
- package/src/local/local-apply-summary.ts +86 -0
- package/src/local/local-apply-watchdog.ts +95 -0
- package/src/local/local-apps.ts +181 -8
- package/src/local/local-blueprint-errors.ts +198 -0
- package/src/local/local-blueprint-outline.ts +31 -12
- package/src/local/local-blueprint-schemas.ts +582 -0
- package/src/local/local-build-inputs.ts +48 -0
- package/src/local/local-build-summary.ts +495 -28
- package/src/local/local-case-evidence.ts +120 -0
- package/src/local/local-case-not-run.ts +153 -0
- package/src/local/local-chat-apps.ts +93 -16
- package/src/local/local-check-inherited.ts +83 -0
- package/src/local/local-cities.ts +89 -0
- package/src/local/local-component-health.ts +119 -0
- package/src/local/local-control.ts +34 -7
- package/src/local/local-db-snapshot.ts +348 -0
- package/src/local/local-dev-references.ts +347 -36
- package/src/local/local-dev-sync.ts +141 -12
- package/src/local/local-docker.ts +35 -1
- package/src/local/local-docs-search.ts +1 -1
- package/src/local/local-docs.ts +281 -2
- package/src/local/local-empty-workflows.ts +92 -0
- package/src/local/local-env.ts +16 -1
- package/src/local/local-file-checks.ts +104 -0
- package/src/local/local-function-ids.ts +141 -0
- package/src/local/local-function-recovery.ts +141 -0
- package/src/local/local-function-state.ts +87 -0
- package/src/local/local-keys.ts +7 -5
- package/src/local/local-knowledge-documents.ts +1 -1
- package/src/local/local-knowledge-ingestion.ts +225 -0
- package/src/local/local-knowledge-uploads.ts +217 -0
- package/src/local/local-lessons.ts +1 -1
- package/src/local/local-library.ts +1 -1
- package/src/local/local-media-placeholders.ts +182 -0
- package/src/local/local-media-sets.ts +90 -10
- package/src/local/local-model-parameters.ts +89 -0
- package/src/local/local-object-sets.ts +18 -6
- package/src/local/local-operating.ts +35 -1
- package/src/local/local-org-settings.ts +206 -8
- package/src/local/local-orphans.ts +321 -0
- package/src/local/local-personas.ts +296 -0
- package/src/local/local-portability.ts +143 -18
- package/src/local/local-ports.ts +99 -7
- package/src/local/local-project-notices.ts +234 -0
- package/src/local/local-project-secrets.ts +133 -0
- package/src/local/local-project.ts +12 -0
- package/src/local/local-provider-refusal.ts +66 -0
- package/src/local/local-request-evidence.ts +112 -0
- package/src/local/local-runtime-health.ts +123 -0
- package/src/local/local-runtime-history.ts +50 -0
- package/src/local/local-runtime-target.ts +42 -0
- package/src/local/local-sample-data.ts +141 -0
- package/src/local/local-sample-remake.ts +138 -0
- package/src/local/local-scaffold.ts +336 -53
- package/src/local/local-schema-check.ts +148 -0
- package/src/local/local-schemas.ts +742 -25
- package/src/local/local-secret-carry.ts +107 -0
- package/src/local/local-secrets.ts +41 -11
- package/src/local/local-seed.ts +102 -26
- package/src/local/local-stand-ins.ts +3 -3
- package/src/local/local-state.ts +43 -2
- package/src/local/local-studio-server.ts +170 -7
- package/src/local/local-supervisor.ts +44 -6
- package/src/local/local-sync-merge.ts +1 -1
- package/src/local/local-sync-names.ts +2 -0
- package/src/local/local-sync-plan.ts +19 -8
- package/src/local/local-sync-push.ts +15 -12
- package/src/local/local-sync-refs.ts +49 -3
- package/src/local/local-sync-remote.ts +349 -38
- package/src/local/local-sync-snapshot.ts +18 -13
- package/src/local/local-synthetic-data.ts +2116 -99
- package/src/local/local-test-baseline.ts +113 -0
- package/src/local/local-test-cases.ts +76 -1
- package/src/local/local-test-runner.ts +582 -91
- package/src/local/local-test-runs.ts +13 -5
- package/src/local/local-vendored-sdk.ts +183 -0
- package/src/local/local-workflow-state.ts +16 -3
- package/src/local/local-workflow-tests.ts +482 -0
- package/src/local/local-workspace-grants.ts +81 -0
- package/src/main.ts +50 -9
- package/src/mcp/a11y-scan.ts +55 -0
- package/src/mcp/browser/ax-snapshot.ts +181 -0
- package/src/mcp/browser/credentials.ts +78 -0
- package/src/mcp/browser/frames.ts +336 -0
- package/src/mcp/browser/headless-session.ts +413 -0
- package/src/mcp/browser/headless-tab.ts +575 -0
- package/src/mcp/browser/keys.ts +70 -0
- package/src/mcp/browser/page-scripts.ts +129 -0
- package/src/mcp/browser/wait.ts +73 -0
- package/src/mcp/forge-link.ts +126 -0
- package/src/mcp/headless.ts +199 -0
- package/src/mcp/mcp-apps.ts +435 -0
- package/src/mcp/mcp-chat.ts +226 -0
- package/src/mcp/mcp-cli.ts +53 -0
- package/src/mcp/mcp-protocol.ts +122 -0
- package/src/mcp/mcp-runtime.ts +113 -0
- package/src/mcp/mcp-server.ts +108 -0
- package/src/mcp/mcp-traces.ts +409 -0
- package/src/mcp/page-observation.ts +193 -0
- package/src/mcp/tools/agent-tools.ts +700 -0
- package/src/mcp/tools/app-tools.ts +241 -0
- package/src/mcp/tools/browser-tools.ts +310 -0
- package/src/mcp/tools/docs-tools.ts +32 -0
- package/src/mcp/tools/project-tools.ts +205 -0
- package/src/mcp/tools/test-tools.ts +339 -0
- package/src/mcp/tools/workflow-tools.ts +611 -0
- package/src/output.ts +1 -1
- package/src/pack.ts +8 -3
- package/src/packaging.ts +1 -1
- package/src/project-bootstrap.ts +107 -4
- package/src/project-context.ts +18 -1
- package/src/project.ts +27 -2
- package/src/provenance.ts +25 -0
- package/src/render-evidence.ts +65 -7
- package/src/scopes.ts +1 -1
- package/src/template.ts +14 -8
- package/src/templates/next-app-files.ts +35 -6
- package/src/templates/next-skills.ts +61 -27
- package/src/vendor/kit-assets.json +6 -6
- package/src/vendor/sdk-sources.json +17 -14
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { basename } from 'node:path'
|
|
2
|
+
import { basename, join } from 'node:path'
|
|
3
3
|
|
|
4
4
|
import { CliError, UsageError } from '../../errors'
|
|
5
5
|
import { canPrompt, promptSecret } from '../../prompt'
|
|
6
6
|
import { allTargets } from '../../local/local-dev-sync'
|
|
7
7
|
import { pullDocuments, type DocumentsPulled } from '../../local/local-knowledge-documents'
|
|
8
|
+
import { appCodeLine, appCodeVersion, earlierPullShadows, pullAppCode, type AppCodePulled } from '../../local/local-app-code'
|
|
9
|
+
import { remakeLines, remakeReason, remakeSampleData, type SampleRemake } from '../../local/local-sample-remake'
|
|
8
10
|
import { lessonsToPropose, pullLessons, type LessonsPulled } from '../../local/local-lessons'
|
|
9
11
|
import { analyticsNeeded, connectionsNeeded, documentationNeeded, standInPath, writePlatformOnly, writeStandIns } from '../../local/local-stand-ins'
|
|
10
12
|
import { localProject, type LocalProject } from '../../local/local-project'
|
|
@@ -36,6 +38,8 @@ import { readWorkspace, type SyncSource } from '../../local/local-sync-remote'
|
|
|
36
38
|
import { buildStatusText, projectBuildSummary } from '../../local/local-build-summary'
|
|
37
39
|
import { kindTitle } from '../../local/local-sync-names'
|
|
38
40
|
import { describeUnresolved } from '../../local/local-sync-refs'
|
|
41
|
+
import { blockers, type Blocker, type Need } from '../../local/local-portability'
|
|
42
|
+
import { hardCodedIdLine, hardCodedObjectTypeIds, platformObjectTypeIds } from '../../local/local-function-ids'
|
|
39
43
|
import {
|
|
40
44
|
directoryOf,
|
|
41
45
|
dropBase,
|
|
@@ -555,8 +559,11 @@ const pull = async (ctx: CommandContext) => {
|
|
|
555
559
|
}
|
|
556
560
|
const named = ctx.positional.slice(1)
|
|
557
561
|
const byRef = new Map(remote.items.map((i) => [i.ref, i]))
|
|
558
|
-
// Knowledge bases whose documents come too:
|
|
562
|
+
// Knowledge bases whose documents come too: every base this pull writes (a pulled Agent answers from them),
|
|
563
|
+
// unless --no-documents; --documents names more, already in the project, to bring theirs again.
|
|
564
|
+
const noDocuments = flagBool(ctx, 'no-documents')
|
|
559
565
|
const documents = refsFlag(ctx, 'documents').map((r) => r.replace(/^knowledge\//, ''))
|
|
566
|
+
if (noDocuments && documents.length) throw new UsageError('--documents and --no-documents ask opposite things', 'leave one out: --no-documents pulls no documents; --documents <name> pulls those bases\' documents')
|
|
560
567
|
const noBase = documents.filter((n) => !byRef.has(`knowledge/${n}`))
|
|
561
568
|
if (noBase.length) throw new UsageError(`no knowledge base ${noBase.join(', ')} in the workspace`, '--documents takes knowledge base names, as `--preview` lists them (knowledge/<name>)')
|
|
562
569
|
const byPlan = new Map(plan.map((p) => [p.ref, p]))
|
|
@@ -608,6 +615,9 @@ const pull = async (ctx: CommandContext) => {
|
|
|
608
615
|
const replaced = [...take].filter((r) => status.get(r) === 'edited' || status.get(r) === 'deleted-local')
|
|
609
616
|
const merge = new Set([...take].filter((r) => status.get(r) === 'conflict'))
|
|
610
617
|
let base = lock?.workspace === link.workspace ? lock : null
|
|
618
|
+
// An App folder an earlier pull left unchanged with package.json and bun.lock disagreeing (an older CLI's repair):
|
|
619
|
+
// told now, before this pull rewrites its frontera.lock entry, so its code is brought again in its place below.
|
|
620
|
+
const earlierPull = new Set([...take].filter((r) => r.startsWith('app/') && earlierPullShadows(project.root, r.slice('app/'.length)).length))
|
|
611
621
|
const readFromBase = (path: string) => readBase(project.root, path)
|
|
612
622
|
const removedDeleted: string[] = []
|
|
613
623
|
for (const [refs, deleteFiles] of [[deleteHere, true], [keepLocal, false]] as const) {
|
|
@@ -622,7 +632,8 @@ const pull = async (ctx: CommandContext) => {
|
|
|
622
632
|
const conflictedRefs = new Set(result.conflicted.map((c) => c.ref))
|
|
623
633
|
writeBase(project.root, items.filter((i) => !conflictedRefs.has(i.ref)), base)
|
|
624
634
|
const pulled = [...take].filter((r) => !conflictedRefs.has(r))
|
|
625
|
-
// Documents
|
|
635
|
+
// Documents land beside each base's knowledge.json; `local dev` uploads them. The bases pulled now bring theirs.
|
|
636
|
+
if (!noDocuments) for (const r of pulled) if (r.startsWith('knowledge/') && !documents.includes(r.slice('knowledge/'.length))) documents.push(r.slice('knowledge/'.length))
|
|
626
637
|
const docs: Record<string, DocumentsPulled> = {}
|
|
627
638
|
if (documents.length) {
|
|
628
639
|
const client = sourceFor(project, link).workspace
|
|
@@ -631,6 +642,34 @@ const pull = async (ctx: CommandContext) => {
|
|
|
631
642
|
docs[n] = id ? await pullDocuments(project.root, client, { name: n, id }) : { written: [], present: [], skipped: [], failed: ['its id is not in the fetched snapshot: fetch again'] }
|
|
632
643
|
}
|
|
633
644
|
}
|
|
645
|
+
// Each pulled Custom App's code comes too, into apps/<slug>/, unless --no-app-code; a folder already there is kept,
|
|
646
|
+
// except one an earlier pull left unchanged whose package.json and bun.lock disagree: the deployed source replaces it.
|
|
647
|
+
const appCode: AppCodePulled[] = []
|
|
648
|
+
if (!flagBool(ctx, 'no-app-code')) {
|
|
649
|
+
const wantCode = pulled.flatMap((r) => {
|
|
650
|
+
if (!r.startsWith('app/')) return []
|
|
651
|
+
const slug = r.slice('app/'.length)
|
|
652
|
+
const it = byRef.get(r)
|
|
653
|
+
const version = appCodeVersion(project.root, slug, it?.version)
|
|
654
|
+
const replaceEarlierPull = earlierPull.has(r)
|
|
655
|
+
return it?.id && version && (replaceEarlierPull || !existsSync(join(project.root, 'apps', slug))) ? [{ slug, id: it.id, version, ...(replaceEarlierPull ? { replaceEarlierPull } : {}) }] : []
|
|
656
|
+
})
|
|
657
|
+
if (wantCode.length) {
|
|
658
|
+
const client = sourceFor(project, link).workspace
|
|
659
|
+
for (const app of wantCode) appCode.push(await pullAppCode(project.root, client, app))
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// Sample data made again when the pull brought Blueprint items, or an older generator made it, unless --keep-sample-data.
|
|
663
|
+
let sampleData: SampleRemake | null = null
|
|
664
|
+
let sampleDataError: string | null = null
|
|
665
|
+
const remakeWhy = flagBool(ctx, 'keep-sample-data') ? null : remakeReason(project.root, pulled.some((r) => r.startsWith('blueprint/')))
|
|
666
|
+
if (remakeWhy) {
|
|
667
|
+
try {
|
|
668
|
+
sampleData = await remakeSampleData(project.root, remakeWhy, new Date(), 'pull')
|
|
669
|
+
} catch (err) {
|
|
670
|
+
sampleDataError = (err as Error).message.slice(0, 200)
|
|
671
|
+
}
|
|
672
|
+
}
|
|
634
673
|
// Built-in connections the project's items use get a declared stand-in (`local-stand-ins.ts`).
|
|
635
674
|
const held = new Set(Object.keys(readLock(project.root)?.items ?? {}))
|
|
636
675
|
const heldItems = remote.items.filter((i) => held.has(i.ref))
|
|
@@ -667,6 +706,11 @@ const pull = async (ctx: CommandContext) => {
|
|
|
667
706
|
` ${n}: ${plural(d.written.length, 'document')} written${d.present.length ? `, ${d.present.length} already here` : ''}${d.skipped.length ? `, ${d.skipped.length} not a kind local dev uploads (${d.skipped.join(', ')})` : ''}`,
|
|
668
707
|
...d.failed.map((f) => ` could not read ${f}`),
|
|
669
708
|
])
|
|
709
|
+
// What won't run locally as pulled, with the reason and whose fix it is: the checks `local dev` applies.
|
|
710
|
+
const wontRun = wontRunLocally(project.root, pulled.map((r) => byRef.get(r)!).filter(Boolean))
|
|
711
|
+
// Functions whose code names an object type by the workspace's id (`local dev` adapts their bundle here).
|
|
712
|
+
const platformIds = platformObjectTypeIds(directoryOf(snapshot))
|
|
713
|
+
const hardCoded = pulled.flatMap((r) => (r.startsWith('function/') && byRef.get(r) ? hardCodedObjectTypeIds(byRef.get(r)!.files, platformIds) : []))
|
|
670
714
|
// The workspace's copies beside conflicted files are not project files; `local dev` ignores them.
|
|
671
715
|
const projectWritten = result.written.filter((p) => !p.endsWith(WORKSPACE_SUFFIX))
|
|
672
716
|
const conflictLines = result.conflicted.flatMap((c) => [
|
|
@@ -689,8 +733,12 @@ const pull = async (ctx: CommandContext) => {
|
|
|
689
733
|
warnings: remote.warnings,
|
|
690
734
|
bases,
|
|
691
735
|
documents: docs,
|
|
736
|
+
appCode,
|
|
737
|
+
...(sampleData ? { sampleData: { why: sampleData.why, options: sampleData.options, datasets: sampleData.written.map((d) => d.dataset), kept: sampleData.kept.map((d) => d.dataset) } } : {}),
|
|
692
738
|
...(lessons ? { lessons } : {}),
|
|
693
739
|
...(standIns.written.length ? { standIns: standIns.written } : {}),
|
|
740
|
+
...(wontRun.length ? { wontRunLocally: wontRun } : {}),
|
|
741
|
+
...(hardCoded.length ? { hardCodedIds: hardCoded } : {}),
|
|
694
742
|
},
|
|
695
743
|
text: [
|
|
696
744
|
pulled.length ? `Pulled ${plural(pulled.length, 'item')} from ${link.workspaceName ?? link.workspace}:` : result.conflicted.length || waiting.length || deleteHere.length || keepLocal.length || lessons ? '' : 'Nothing to pull.',
|
|
@@ -703,11 +751,28 @@ const pull = async (ctx: CommandContext) => {
|
|
|
703
751
|
'See them with `frontera local sync diff <ref>`; settle with `resolve <ref> --keep workspace|mine`, or edit the file and `resolve <ref> --edited`.',
|
|
704
752
|
]
|
|
705
753
|
: []),
|
|
754
|
+
...(wontRun.length
|
|
755
|
+
? [
|
|
756
|
+
'',
|
|
757
|
+
`Won’t run locally as pulled (${plural(wontRun.length, 'item')}; \`local dev\` leaves ${wontRun.length === 1 ? 'it' : 'them'} out and says so):`,
|
|
758
|
+
...wontRun.flatMap((w) => [` ${w.ref}: ${w.reason}`, ` fix: ${w.fix} — ${w.owner}`]),
|
|
759
|
+
]
|
|
760
|
+
: []),
|
|
761
|
+
...(hardCoded.length
|
|
762
|
+
? [
|
|
763
|
+
'',
|
|
764
|
+
'Object type ids hard-coded in Function code (the workspace’s ids; `local dev` deploys these Functions here with this runtime’s ids in their place and leaves the files as they are):',
|
|
765
|
+
...hardCoded.map((h) => ` warning: ${hardCodedIdLine(h)}`),
|
|
766
|
+
]
|
|
767
|
+
: []),
|
|
706
768
|
...(waiting.length ? ['', `Not pulled, conflicts to resolve first: ${waiting.join(', ')}`] : []),
|
|
707
769
|
...(deleteHere.length ? ['', `Deleted here, as in the workspace: ${deleteHere.join(', ')}`] : []),
|
|
708
770
|
...(keepLocal.length ? ['', `Kept as this project’s own: ${keepLocal.join(', ')}`] : []),
|
|
709
771
|
...(projectWritten.length ? ['', `Wrote ${plural(projectWritten.length, 'file')}; \`frontera local dev\` applies them.`] : []),
|
|
710
772
|
...(docLines.length ? ['', 'Documents (uploaded to the local knowledge base by `frontera local dev`):', ...docLines] : []),
|
|
773
|
+
...(sampleData ? ['', ...remakeLines(sampleData)] : []),
|
|
774
|
+
...(sampleDataError ? ['', `Sample data couldn’t be made again (${sampleDataError}); \`frontera local data --synthetic\` tries again`] : []),
|
|
775
|
+
...(appCode.length ? ['', 'App code (built by `frontera local dev`; `app_dev` runs it against the local runtime):', ...appCode.map(appCodeLine)] : []),
|
|
711
776
|
...lessonLines,
|
|
712
777
|
...standInLines,
|
|
713
778
|
...remote.warnings.map((w) => `Could not read ${w}`),
|
|
@@ -718,6 +783,23 @@ const pull = async (ctx: CommandContext) => {
|
|
|
718
783
|
}
|
|
719
784
|
}
|
|
720
785
|
|
|
786
|
+
/**
|
|
787
|
+
* The pulled items that won't run on the local runtime as they are, each with
|
|
788
|
+
* why, the fix and whose fix it is. A file a Function imports that the project
|
|
789
|
+
* now has (written by hand, or by another pull) no longer counts.
|
|
790
|
+
*/
|
|
791
|
+
export function wontRunLocally(root: string, items: ReadonlyArray<Pick<RemoteItem, 'ref' | 'portability'>>): Array<Blocker & { ref: string }> {
|
|
792
|
+
const present = (stem: string) => ['', '.ts', '.tsx', '.mts', '.js', '.mjs', '/index.ts', '/index.js'].some((ext) => existsSync(join(root, `${stem}${ext}`)))
|
|
793
|
+
return items.flatMap((it) => {
|
|
794
|
+
const needs = (it.portability?.needs ?? []).flatMap((n): Need[] => {
|
|
795
|
+
if (n.kind !== 'imports-missing') return [n]
|
|
796
|
+
const paths = n.paths.filter((p) => !present(p))
|
|
797
|
+
return paths.length ? [{ kind: 'imports-missing', paths }] : []
|
|
798
|
+
})
|
|
799
|
+
return blockers(needs).map((b) => ({ ref: it.ref, ...b }))
|
|
800
|
+
})
|
|
801
|
+
}
|
|
802
|
+
|
|
721
803
|
/** Everything about one item for `diff` and `resolve`: the plan row, the workspace's version, and the three-way merge. */
|
|
722
804
|
async function itemState(project: LocalProject, link: SyncLink, ref: string) {
|
|
723
805
|
// Offline, as diff and resolve are: a lost base comes back only from an unchanged copy here (pull reads the platform).
|
|
@@ -727,7 +809,7 @@ async function itemState(project: LocalProject, link: SyncLink, ref: string) {
|
|
|
727
809
|
const item = remote.items.find((i) => i.ref === ref) ?? null
|
|
728
810
|
const entry = lock?.workspace === link.workspace ? lock.items[ref] : undefined
|
|
729
811
|
if (!row && !item && !entry) throw new UsageError(`not in the workspace or the project: ${ref}`, 'list items with `frontera local sync pull --preview`')
|
|
730
|
-
const paths = [...new Set([...Object.keys(item?.files ?? {}), ...Object.keys(entry?.files ?? {}), ...(entry?.conflict?.files ?? [])])].sort()
|
|
812
|
+
const paths = [...new Set([...Object.keys(item?.files ?? {}), ...Object.keys(entry?.files ?? {}), ...(entry?.conflict?.files ?? [])])].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
|
|
731
813
|
const base = baseFiles(entry, (p) => readBase(project.root, p))
|
|
732
814
|
const local = localFiles(project.root, paths)
|
|
733
815
|
const theirs = item?.files ?? {}
|
|
@@ -901,7 +983,17 @@ export const localSync: Command = {
|
|
|
901
983
|
description: 'string',
|
|
902
984
|
migration: 'string',
|
|
903
985
|
documents: 'string',
|
|
986
|
+
'no-documents': 'boolean',
|
|
987
|
+
'no-app-code': 'boolean',
|
|
988
|
+
'keep-sample-data': 'boolean',
|
|
904
989
|
lessons: 'boolean',
|
|
990
|
+
release: 'string',
|
|
991
|
+
force: 'boolean',
|
|
992
|
+
reason: 'string',
|
|
993
|
+
evals: 'boolean',
|
|
994
|
+
renders: 'boolean',
|
|
995
|
+
screenshots: 'string',
|
|
996
|
+
trace: 'string',
|
|
905
997
|
},
|
|
906
998
|
aliases: { t: 'title', d: 'description' },
|
|
907
999
|
summary: 'Link the project to a platform workspace, pull and merge what you choose, and propose changes back',
|
|
@@ -913,7 +1005,10 @@ export const localSync: Command = {
|
|
|
913
1005
|
'frontera local sync pull --preview',
|
|
914
1006
|
'frontera local sync pull agent/support-concierge workflow/return-label',
|
|
915
1007
|
'frontera local sync pull --kinds blueprint,workflows',
|
|
916
|
-
'frontera local sync pull knowledge/faq
|
|
1008
|
+
'frontera local sync pull knowledge/faq',
|
|
1009
|
+
'frontera local sync pull --kinds knowledge --no-documents',
|
|
1010
|
+
'frontera local sync pull --documents faq',
|
|
1011
|
+
'frontera local sync pull app/ops-console --no-app-code',
|
|
917
1012
|
'frontera local sync pull agent/support-concierge --lessons',
|
|
918
1013
|
'frontera local sync diff agent/support-concierge',
|
|
919
1014
|
'frontera local sync resolve agent/support-concierge --keep workspace',
|
|
@@ -924,12 +1019,14 @@ export const localSync: Command = {
|
|
|
924
1019
|
'frontera local sync push agent/support-concierge -t "New greeting"',
|
|
925
1020
|
'frontera local sync push --documents faq -t "Updated returns policy"',
|
|
926
1021
|
'frontera local sync request',
|
|
1022
|
+
'frontera local sync push --release desk=1.4.0 -t "Release the desk"',
|
|
1023
|
+
'frontera local sync request evidence --evals --renders',
|
|
927
1024
|
'frontera local sync request apply',
|
|
1025
|
+
'frontera local sync request apply --force --reason "hotfix, checked by hand"',
|
|
928
1026
|
'frontera local sync request close',
|
|
929
1027
|
'frontera local sync disconnect --forget-link',
|
|
930
1028
|
],
|
|
931
1029
|
offline: true,
|
|
932
|
-
internal: true,
|
|
933
1030
|
},
|
|
934
1031
|
async run(ctx) {
|
|
935
1032
|
const action = ctx.positional[0]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { localProject } from '../../local/local-project'
|
|
2
|
+
import { stopStartedDevServers } from '../../mcp/mcp-apps'
|
|
3
|
+
import { serveMcp } from '../../mcp/mcp-protocol'
|
|
4
|
+
import { MCP_INFO, mcpTools } from '../../mcp/mcp-server'
|
|
5
|
+
import type { Command } from '../types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Frontera as one MCP server on stdio for the coding agents working in a
|
|
9
|
+
* project: the local runtime (status, apply, chat with an Agent, traces,
|
|
10
|
+
* tests, workflows, records), the documentation, App rendering and, with
|
|
11
|
+
* Forge open, a browser the person watches. Registered once per project as
|
|
12
|
+
* `frontera` in `.mcp.json`, `.cursor/mcp.json`, `opencode.json` and
|
|
13
|
+
* `.codex/config.toml`. Every tool takes an optional `project` folder, so
|
|
14
|
+
* one connection also serves the other projects the agent works in. Nothing
|
|
15
|
+
* but protocol goes to stdout.
|
|
16
|
+
*/
|
|
17
|
+
export const mcpServe: Command = {
|
|
18
|
+
meta: {
|
|
19
|
+
noun: 'mcp',
|
|
20
|
+
verb: '',
|
|
21
|
+
args: [],
|
|
22
|
+
flags: {},
|
|
23
|
+
summary: 'Serve Frontera to coding agents over MCP (stdio): runtime, docs, Apps and the Forge browser',
|
|
24
|
+
examples: ['frontera mcp'],
|
|
25
|
+
offline: true,
|
|
26
|
+
},
|
|
27
|
+
async run(ctx) {
|
|
28
|
+
await serveMcp({ info: MCP_INFO, tools: mcpTools(localProject(ctx.cwd), ctx.cwd) })
|
|
29
|
+
// The connection closed: the App dev servers this session started stop with it (each also watches this process).
|
|
30
|
+
stopStartedDevServers()
|
|
31
|
+
// The stream is the protocol: exit here, before the CLI renders a result onto it.
|
|
32
|
+
process.exit(0)
|
|
33
|
+
},
|
|
34
|
+
}
|
package/src/commands/registry.ts
CHANGED
|
@@ -12,11 +12,17 @@ import { appSave } from './app/save'
|
|
|
12
12
|
import { appDeploy } from './app/deploy'
|
|
13
13
|
import { appPromote } from './app/promote'
|
|
14
14
|
import { appVersions } from './app/versions'
|
|
15
|
+
import { appChecks } from './app/checks'
|
|
16
|
+
import { appDeployments } from './app/deployments'
|
|
17
|
+
import { appRollback } from './app/rollback'
|
|
18
|
+
import { appUnpin } from './app/unpin'
|
|
15
19
|
import { appSdk } from './app/sdk'
|
|
16
20
|
import { appDev } from './app/dev'
|
|
17
21
|
import { blueprintList } from './blueprint/list'
|
|
18
22
|
import { blueprintGet } from './blueprint/get'
|
|
23
|
+
import { blueprintObjectSetsList } from './blueprint/object-sets'
|
|
19
24
|
import { blueprintInstance, blueprintQuery } from './blueprint/query'
|
|
25
|
+
import { blueprintMetric } from './blueprint/metric'
|
|
20
26
|
import { blueprintGenerateTypes } from './blueprint/generate-types'
|
|
21
27
|
import { blueprintAuthoringCommands } from './blueprint/authoring'
|
|
22
28
|
import { blueprintDeclarativeCommands } from './blueprint/declarative'
|
|
@@ -47,6 +53,8 @@ import { kitCommands } from './kit/index-commands'
|
|
|
47
53
|
import { workspaceCommands } from './workspace/index-commands'
|
|
48
54
|
import { forgeCommands } from './forge/index-commands'
|
|
49
55
|
import { localCommands } from './local/index-commands'
|
|
56
|
+
import { localSnapshot } from './local/snapshot'
|
|
57
|
+
import { mcpServe } from './mcp/serve'
|
|
50
58
|
|
|
51
59
|
/**
|
|
52
60
|
* Flags every command accepts. Declared once so a caller never has to wonder
|
|
@@ -88,6 +96,8 @@ export const COMMANDS: readonly Command[] = [
|
|
|
88
96
|
...workspaceCommands,
|
|
89
97
|
...forgeCommands,
|
|
90
98
|
...localCommands,
|
|
99
|
+
localSnapshot,
|
|
100
|
+
mcpServe,
|
|
91
101
|
|
|
92
102
|
appInit,
|
|
93
103
|
appList,
|
|
@@ -96,6 +106,10 @@ export const COMMANDS: readonly Command[] = [
|
|
|
96
106
|
appSave,
|
|
97
107
|
appDeploy,
|
|
98
108
|
appPromote,
|
|
109
|
+
appRollback,
|
|
110
|
+
appUnpin,
|
|
111
|
+
appDeployments,
|
|
112
|
+
appChecks,
|
|
99
113
|
appVersions,
|
|
100
114
|
appSdk,
|
|
101
115
|
appDev,
|
|
@@ -119,7 +133,9 @@ export const COMMANDS: readonly Command[] = [
|
|
|
119
133
|
// Reads, next to the other two reads: the authoring verbs below all report
|
|
120
134
|
// success without proving anything came back, and these are what prove it.
|
|
121
135
|
blueprintQuery,
|
|
136
|
+
blueprintMetric,
|
|
122
137
|
blueprintInstance,
|
|
138
|
+
blueprintObjectSetsList,
|
|
123
139
|
blueprintGenerateTypes,
|
|
124
140
|
// Authoring is no longer reserved: the organization API key (`sk-org-`) reaches the
|
|
125
141
|
// organization-level shared draft, which is what these verbs were waiting on.
|
|
@@ -151,8 +167,21 @@ export function findCommand(noun: string, verb: string | undefined): Command | n
|
|
|
151
167
|
)
|
|
152
168
|
}
|
|
153
169
|
|
|
170
|
+
/**
|
|
171
|
+
* The local loop: `frontera local …` (a runtime on this machine, applying the
|
|
172
|
+
* project, its tests and its gate) and `frontera mcp` (the same for a coding
|
|
173
|
+
* agent). Listed together in help, under one heading, after the platform's
|
|
174
|
+
* nouns.
|
|
175
|
+
*/
|
|
176
|
+
export const LOCAL_DEVELOPMENT_NOUNS = ['local', 'mcp'] as const
|
|
177
|
+
|
|
178
|
+
/** Whether a command is in the top-level help, `help --json` and the generated reference. */
|
|
179
|
+
export function isListed(meta: CommandMeta): boolean {
|
|
180
|
+
return !meta.internal
|
|
181
|
+
}
|
|
182
|
+
|
|
154
183
|
export function nouns(includeInternal = false): string[] {
|
|
155
|
-
return [...new Set(COMMANDS.filter((c) => includeInternal ||
|
|
184
|
+
return [...new Set(COMMANDS.filter((c) => includeInternal || isListed(c.meta)).map((c) => c.meta.noun))]
|
|
156
185
|
}
|
|
157
186
|
|
|
158
187
|
/**
|
|
@@ -183,6 +212,8 @@ const NOUN_SUMMARY: Readonly<Record<string, string>> = {
|
|
|
183
212
|
kit: 'Optional project-local Frontera skills for Codex and Claude Code',
|
|
184
213
|
workspace: 'Workspaces here — the ids --workspace takes',
|
|
185
214
|
forge: 'Forge — the local daemon the desktop app talks to',
|
|
215
|
+
mcp: 'Frontera for coding agents — runtime, docs, Apps, browser',
|
|
216
|
+
local: 'A runtime on this machine — apply, test and check the project',
|
|
186
217
|
}
|
|
187
218
|
|
|
188
219
|
export function nounSummary(noun: string): string {
|
|
@@ -308,8 +339,9 @@ export function renderHelp(): string {
|
|
|
308
339
|
]
|
|
309
340
|
|
|
310
341
|
// Setup first and ungrouped: these are the two commands someone runs before
|
|
311
|
-
// any noun makes sense.
|
|
312
|
-
const
|
|
342
|
+
// any noun makes sense. `frontera mcp` is listed with the local loop instead.
|
|
343
|
+
const local = new Set<string>(LOCAL_DEVELOPMENT_NOUNS)
|
|
344
|
+
const setup = COMMANDS.filter((c) => c.meta.verb === '' && isListed(c.meta) && !local.has(c.meta.noun))
|
|
313
345
|
if (setup.length > 0) {
|
|
314
346
|
lines.push('', heading('Getting started'))
|
|
315
347
|
lines.push(
|
|
@@ -317,8 +349,8 @@ export function renderHelp(): string {
|
|
|
317
349
|
)
|
|
318
350
|
}
|
|
319
351
|
|
|
320
|
-
for (const noun of nouns()) {
|
|
321
|
-
const group = COMMANDS.filter((c) => c.meta.noun === noun && c.meta.verb !== '')
|
|
352
|
+
for (const noun of nouns().filter((n) => !local.has(n))) {
|
|
353
|
+
const group = COMMANDS.filter((c) => c.meta.noun === noun && c.meta.verb !== '' && isListed(c.meta))
|
|
322
354
|
if (group.length === 0) continue
|
|
323
355
|
|
|
324
356
|
lines.push('', `${heading(accent(`frontera ${noun}`))} ${dim(nounSummary(noun))}`.trimEnd())
|
|
@@ -340,6 +372,13 @@ export function renderHelp(): string {
|
|
|
340
372
|
}
|
|
341
373
|
}
|
|
342
374
|
|
|
375
|
+
// The local loop, as one group: every `frontera local` verb, then `frontera mcp`.
|
|
376
|
+
const localGroup = COMMANDS.filter((c) => local.has(c.meta.noun) && isListed(c.meta) && !c.meta.reserved)
|
|
377
|
+
if (localGroup.length > 0) {
|
|
378
|
+
lines.push('', `${heading('Local development')} ${dim('a runtime on this machine, for you and your coding agent')}`)
|
|
379
|
+
lines.push(...definitions(localGroup.map((c) => [usage(c.meta).replace(/^frontera /, ''), c.meta.summary] as const), { indent: 4 }))
|
|
380
|
+
}
|
|
381
|
+
|
|
343
382
|
lines.push(
|
|
344
383
|
...GLOBAL_SECTION,
|
|
345
384
|
'',
|
|
@@ -398,6 +437,8 @@ export const SESSION_LANE_HELP =
|
|
|
398
437
|
export function renderCommandHelp(meta: CommandMeta): string {
|
|
399
438
|
const width = terminalWidth()
|
|
400
439
|
const lines = [accent(usage(meta)), '', ...wrap(meta.summary, width)]
|
|
440
|
+
// A paragraph that starts indented is code, printed as written.
|
|
441
|
+
for (const paragraph of meta.details ?? []) lines.push('', ...(paragraph.startsWith(' ') ? paragraph.split('\n') : wrap(paragraph, width)))
|
|
401
442
|
|
|
402
443
|
if (meta.reserved) {
|
|
403
444
|
lines.push('', ...wrap(`Not available yet: ${meta.reserved}`, width).map(dim))
|
|
@@ -435,7 +476,7 @@ export function renderCommandHelp(meta: CommandMeta): string {
|
|
|
435
476
|
// Command flags first, then the ones every command shares — what is specific
|
|
436
477
|
// to this command is what the reader came for.
|
|
437
478
|
const shared = new Set(Object.keys(GLOBAL_FLAGS))
|
|
438
|
-
const own = Object.keys(spec).filter((f) => !shared.has(f)).sort()
|
|
479
|
+
const own = Object.keys(spec).filter((f) => !shared.has(f)).sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
|
|
439
480
|
const short = Object.entries(aliasesFor(meta)).reduce<Record<string, string>>((acc, [s, long]) => {
|
|
440
481
|
acc[long] = s
|
|
441
482
|
return acc
|
|
@@ -445,7 +486,7 @@ export function renderCommandHelp(meta: CommandMeta): string {
|
|
|
445
486
|
lines.push('', heading('Flags'))
|
|
446
487
|
lines.push(
|
|
447
488
|
...definitions(
|
|
448
|
-
own.map((f) => [flagUsage(f, spec[f]!, short[f]), describeFlag(f)] as const),
|
|
489
|
+
own.map((f) => [flagUsage(f, spec[f]!, short[f]), describeFlag(f, meta.flagHelp)] as const),
|
|
449
490
|
),
|
|
450
491
|
)
|
|
451
492
|
}
|
|
@@ -471,6 +512,8 @@ export interface CommandDescriptor {
|
|
|
471
512
|
verb: string
|
|
472
513
|
usage: string
|
|
473
514
|
summary: string
|
|
515
|
+
/** The paragraphs `--help` prints under the summary. */
|
|
516
|
+
details?: string[]
|
|
474
517
|
available: boolean
|
|
475
518
|
unavailableReason?: string
|
|
476
519
|
args: Array<{ name: string; required: boolean; description: string }>
|
|
@@ -497,16 +540,17 @@ export function describeCommand(meta: CommandMeta): CommandDescriptor {
|
|
|
497
540
|
verb: meta.verb,
|
|
498
541
|
usage: usage(meta),
|
|
499
542
|
summary: meta.summary,
|
|
543
|
+
...(meta.details?.length ? { details: [...meta.details] } : {}),
|
|
500
544
|
available: !meta.reserved,
|
|
501
545
|
...(meta.reserved ? { unavailableReason: meta.reserved } : {}),
|
|
502
546
|
args: meta.args.map((a) => ({ ...a })),
|
|
503
547
|
flags: Object.keys(spec)
|
|
504
|
-
.sort()
|
|
548
|
+
.sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))
|
|
505
549
|
.map((name) => ({
|
|
506
550
|
name,
|
|
507
551
|
type: spec[name]!,
|
|
508
552
|
...(short[name] ? { short: short[name]! } : {}),
|
|
509
|
-
description: describeFlag(name),
|
|
553
|
+
description: describeFlag(name, meta.flagHelp),
|
|
510
554
|
})),
|
|
511
555
|
examples: [...meta.examples],
|
|
512
556
|
needsProject: Boolean(meta.needsProject),
|
|
@@ -516,7 +560,7 @@ export function describeCommand(meta: CommandMeta): CommandDescriptor {
|
|
|
516
560
|
}
|
|
517
561
|
|
|
518
562
|
export function describeCommands(noun?: string): CommandDescriptor[] {
|
|
519
|
-
return COMMANDS.filter((c) => (noun ? c.meta.noun === noun :
|
|
563
|
+
return COMMANDS.filter((c) => (noun ? c.meta.noun === noun : isListed(c.meta))).map((c) => describeCommand(c.meta))
|
|
520
564
|
}
|
|
521
565
|
|
|
522
566
|
/** Levenshtein distance, iterative with one row — enough for a typo check. */
|
package/src/commands/types.ts
CHANGED
|
@@ -21,9 +21,21 @@ export interface CommandMeta {
|
|
|
21
21
|
verb: string
|
|
22
22
|
args: readonly ArgSpec[]
|
|
23
23
|
flags: FlagSpec
|
|
24
|
+
/**
|
|
25
|
+
* This command's wording for a flag whose shared description (`FLAG_HELP`)
|
|
26
|
+
* would mislead here: `local test --file` names a case file to run, not a
|
|
27
|
+
* document to send. Rare by design; a flag still needs its `FLAG_HELP` entry.
|
|
28
|
+
*/
|
|
29
|
+
flagHelp?: Readonly<Record<string, string>>
|
|
24
30
|
/** Short forms, e.g. `{ f: 'file' }` for `-f`. */
|
|
25
31
|
aliases?: Readonly<Record<string, string>>
|
|
26
32
|
summary: string
|
|
33
|
+
/**
|
|
34
|
+
* Paragraphs `--help` prints under the summary: a contract the command reads
|
|
35
|
+
* that no flag describes (the file it runs, the order it fills things in).
|
|
36
|
+
* Agents read --help before documentation, so what they must know is here.
|
|
37
|
+
*/
|
|
38
|
+
details?: readonly string[]
|
|
27
39
|
/** At least one, copy-pasteable. Agents read --help before documentation. */
|
|
28
40
|
examples: readonly string[]
|
|
29
41
|
/**
|
|
@@ -96,6 +108,8 @@ export interface CommandContext {
|
|
|
96
108
|
token: string
|
|
97
109
|
positional: string[]
|
|
98
110
|
flags: Record<string, string | boolean>
|
|
111
|
+
/** Every value of a string flag given more than once (`flags` holds the last). */
|
|
112
|
+
repeated?: Record<string, string[]>
|
|
99
113
|
output: Output
|
|
100
114
|
/** Present when `meta.needsProject`. */
|
|
101
115
|
project: AppProject | null
|
|
@@ -104,6 +118,8 @@ export interface CommandContext {
|
|
|
104
118
|
* acting in. Undefined for a workspace key, which carries its own.
|
|
105
119
|
*/
|
|
106
120
|
workspaceId?: string
|
|
121
|
+
/** Whether a person is at a terminal; `canPrompt()` when absent. Set by tests. */
|
|
122
|
+
interactive?: boolean
|
|
107
123
|
}
|
|
108
124
|
|
|
109
125
|
/**
|
|
@@ -114,6 +130,11 @@ export interface CommandContext {
|
|
|
114
130
|
export interface CommandResult {
|
|
115
131
|
data: unknown
|
|
116
132
|
text: string
|
|
133
|
+
/**
|
|
134
|
+
* The exit code when it is not 0: a command whose result is a verdict (`local check --json`) renders it on
|
|
135
|
+
* stdout and still exits non-zero when the verdict is a failure. Errors are thrown, never returned.
|
|
136
|
+
*/
|
|
137
|
+
exitCode?: number
|
|
117
138
|
}
|
|
118
139
|
|
|
119
140
|
export interface Command {
|
|
@@ -126,6 +147,14 @@ export function flagString(ctx: CommandContext, name: string): string | undefine
|
|
|
126
147
|
return typeof v === 'string' ? v : undefined
|
|
127
148
|
}
|
|
128
149
|
|
|
150
|
+
/** Every value of a string flag, in the order given: `[]` when absent. */
|
|
151
|
+
export function flagStrings(ctx: CommandContext, name: string): string[] {
|
|
152
|
+
const all = ctx.repeated?.[name]
|
|
153
|
+
if (all?.length) return [...all]
|
|
154
|
+
const one = flagString(ctx, name)
|
|
155
|
+
return one === undefined ? [] : [one]
|
|
156
|
+
}
|
|
157
|
+
|
|
129
158
|
export function flagBool(ctx: CommandContext, name: string): boolean {
|
|
130
159
|
return ctx.flags[name] === true
|
|
131
160
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { WorkflowApi } from '../../api/workflow-api'
|
|
2
|
+
import type { Command } from '../types'
|
|
3
|
+
import { requireSlug } from './push'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Archive and restore: the reversible way a workflow leaves service. Every
|
|
7
|
+
* version and run stays; an archived workflow is left out of `workflow list`,
|
|
8
|
+
* nothing starts it (Agents, webhooks, schedules) and it takes no new version
|
|
9
|
+
* until it is restored. The slug stays taken while it is archived.
|
|
10
|
+
*/
|
|
11
|
+
export const workflowArchive: Command = {
|
|
12
|
+
meta: {
|
|
13
|
+
noun: 'workflow',
|
|
14
|
+
verb: 'archive',
|
|
15
|
+
args: [{ name: 'slug', required: true, description: 'workflow slug' }],
|
|
16
|
+
flags: {},
|
|
17
|
+
summary: 'Archive a workflow: nothing starts it, its versions and runs are kept (`workflow restore` puts it back)',
|
|
18
|
+
examples: ['frontera workflow archive hello'],
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
async run(ctx) {
|
|
22
|
+
const slug = requireSlug(ctx)
|
|
23
|
+
const result = await new WorkflowApi(ctx.apiUrl, ctx.token, ctx.workspaceId).archive(slug)
|
|
24
|
+
return {
|
|
25
|
+
data: result,
|
|
26
|
+
text: [`Archived workflow ${slug}: nothing starts it now; its versions and runs are kept.`, ` \`frontera workflow restore ${slug}\` puts it back`].join('\n'),
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const workflowRestore: Command = {
|
|
32
|
+
meta: {
|
|
33
|
+
noun: 'workflow',
|
|
34
|
+
verb: 'restore',
|
|
35
|
+
args: [{ name: 'slug', required: true, description: 'workflow slug' }],
|
|
36
|
+
flags: {},
|
|
37
|
+
summary: 'Put an archived workflow back into service, as it was',
|
|
38
|
+
examples: ['frontera workflow restore hello'],
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
async run(ctx) {
|
|
42
|
+
const slug = requireSlug(ctx)
|
|
43
|
+
const result = await new WorkflowApi(ctx.apiUrl, ctx.token, ctx.workspaceId).restore(slug)
|
|
44
|
+
return { data: result, text: `Restored workflow ${slug}.` }
|
|
45
|
+
},
|
|
46
|
+
}
|
|
@@ -39,7 +39,8 @@ export const workflowGet: Command = {
|
|
|
39
39
|
// Only when bound. An unbound workflow has no subject line to print, and
|
|
40
40
|
// an empty one reads as a missing value rather than an absent concept.
|
|
41
41
|
workflow.subjectApiName ? ` subject ${workflow.subjectLabel ?? workflow.subjectApiName}` : '',
|
|
42
|
-
|
|
42
|
+
// Live runs and test runs apart: a workflow tested twenty times and never live has 0 runs that count.
|
|
43
|
+
` runs ${workflow.runCount ?? 0} live${workflow.lastRunStatus ? `, last ${workflow.lastRunStatus}` : ''}${typeof workflow.testRunCount === 'number' ? ` · ${workflow.testRunCount} test` : ''}`,
|
|
43
44
|
].filter(Boolean)
|
|
44
45
|
|
|
45
46
|
const versionTable = versions.length === 0
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { workflowArchive, workflowRestore } from './archive'
|
|
1
2
|
import { workflowCancel } from './cancel'
|
|
2
3
|
import { workflowGet } from './get'
|
|
3
4
|
import { workflowLineage } from './lineage'
|
|
@@ -6,6 +7,7 @@ import { workflowPromote } from './promote'
|
|
|
6
7
|
import { workflowPush, workflowValidate } from './push'
|
|
7
8
|
import { workflowRun } from './run'
|
|
8
9
|
import { workflowRuns } from './runs'
|
|
10
|
+
import { workflowWebhookTest } from './webhook-test'
|
|
9
11
|
import type { Command } from '../types'
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -37,5 +39,8 @@ export const workflowCommands: readonly Command[] = [
|
|
|
37
39
|
workflowRun,
|
|
38
40
|
workflowRuns,
|
|
39
41
|
workflowCancel,
|
|
42
|
+
workflowArchive,
|
|
43
|
+
workflowRestore,
|
|
40
44
|
workflowLineage,
|
|
45
|
+
workflowWebhookTest,
|
|
41
46
|
]
|