@ztalkai/spec-flow-cli
v0.1.3
Published
SpecFlow supported command-line client
Downloads
446
Readme
SpecFlow CLI
@ztalkai/spec-flow-cli uses OAuth Device Authorization to create an independent
seven-day Better Auth CLI Session. It supports the Node.js 24 runtime line;
Node.js 22 and unverified future major versions are intentionally unsupported.
npm install --global @ztalkai/[email protected]
specflow --server https://specflow.example auth login
specflow --server https://specflow.example auth status
specflow --server https://specflow.example auth logoutAfter login, resource commands reuse the stored CLI Session and emit structured JSON
on stdout. Existing-Project commands always require an explicit --project-id:
SPEC_FLOW_API_URL=https://specflow.example specflow project list
SPEC_FLOW_API_URL=https://specflow.example specflow project list --status Archived
SPEC_FLOW_API_URL=https://specflow.example specflow project create --name "Payments" --project-id PAY
SPEC_FLOW_API_URL=https://specflow.example specflow project create --name "Payments" --project-id PAY --gherkin-language zh-CN
SPEC_FLOW_API_URL=https://specflow.example specflow project archive --project-id <project-id> --expected-version '"1"'
SPEC_FLOW_API_URL=https://specflow.example specflow project restore --project-id <project-id> --expected-version '"2"'
SPEC_FLOW_API_URL=https://specflow.example specflow project directory --project-id <project-id>
SPEC_FLOW_API_URL=https://specflow.example specflow spec read --project-id <project-id> --spec-id <spec-id>
SPEC_FLOW_API_URL=https://specflow.example specflow spec current --project-id <project-id> --spec-id <spec-id>
SPEC_FLOW_API_URL=https://specflow.example specflow spec revision --project-id <project-id> --spec-id <spec-id> --revision-id <revision-id>
SPEC_FLOW_API_URL=https://specflow.example specflow spec changelog --project-id <project-id> --spec-id <spec-id>
SPEC_FLOW_API_URL=https://specflow.example specflow spec edit --project-id <project-id> --spec-id <spec-id> --gherkin-source "Feature: Payments"
SPEC_FLOW_API_URL=https://specflow.example specflow spec assign-scenario-ids --project-id <project-id> --spec-id <spec-id>
SPEC_FLOW_API_URL=https://specflow.example specflow spec publish --project-id <project-id> --spec-id <spec-id>
SPEC_FLOW_API_URL=https://specflow.example specflow glossary edit --project-id <project-id> --entries '[{"id":"payment","canonicalTerm":"Payment","definition":"A completed transfer.","usageNote":null}]'
SPEC_FLOW_API_URL=https://specflow.example specflow proposal create-spec --project-id <project-id> --spec-id <spec-id> --baseline-gherkin-source "Feature: Payments" --proposed-gherkin-source "Feature: Payments" --agent-revision <revision> --agent-model <model> --source-reference-ids '["source-id"]'
SPEC_FLOW_API_URL=https://specflow.example specflow proposal list --project-id <project-id> --target-kind Spec --spec-id <spec-id> --status Open
SPEC_FLOW_API_URL=https://specflow.example specflow proposal apply --project-id <project-id> --proposal-id <proposal-id> --change-ids '["hunk-1"]'
SPEC_FLOW_API_URL=https://specflow.example specflow proposal discard --project-id <project-id> --proposal-id <proposal-id>
SPEC_FLOW_API_URL=https://specflow.example specflow authoring-history --project-id <project-id>Every official CLI service request sends the versioned @ztalkai/spec-flow-cli/<version>
User-Agent. Diagnostics use stderr, and stable Skill Errors use documented reason and
exit-status categories.
Machine contract
Resource-command success writes exactly one newline-terminated JSON object to stdout.
stderr is newline-delimited JSON (NDJSON): zero or more diagnostics use
{ "diagnostic": { "level": "warning", "message": string } }. These
command/result pairs are the supported schemas:
| Command | Success object |
| --- | --- |
| project list [--status Active\|Archived] | { projects: ProjectSummary[] } |
| project create | { project: ProjectSummary } |
| project archive | { project: ProjectSummary } |
| project restore | { project: ProjectSummary } |
| project context | { project: ProjectContext, publishedGlossary: PublishedGlossary \| null } |
| project directory | { capabilities: Capability[], permissions: Permissions, specs: SpecSummary[] } |
| spec search | { nextCursor: string \| null, results: SearchResult[], totalCount: number } |
| spec read | { currentRevision: SpecRevision \| null, permissions: Permissions, spec: SpecDetails } |
| spec current | { revision: SpecRevision, spec: SpecSummary } |
| spec revision | { revision: SpecRevision } |
| spec changelog | { entries: SpecChangelogEntry[] } |
| spec create | { spec: SpecDetails } |
| spec update | { spec: SpecDetails } |
| spec draft | { spec: SpecDetails } |
| spec edit | { draft: { draftSessionId: string, gherkinSource: string, specId: string } } |
| spec assign-scenario-ids | { assignedScenarioIds: string[], draft: { draftSessionId: string, gherkinSource: string, specId: string } } |
| spec publish | { revision: SpecRevision, spec: SpecSummary } |
| spec state | { spec: SpecSummary } |
| spec rollback | { revision: SpecRevision, spec: SpecSummary } |
| spec delete | { deleted: { specId: string } } |
| glossary read | { currentRevision: GlossaryRevision \| null, draftSession: GlossaryDraftSession \| null, permissions: Permissions } |
| glossary changelog | { entries: GlossaryChangelogEntry[] } |
| glossary revision | { revision: GlossaryRevision } |
| glossary draft | { glossary: GlossaryDetails } |
| glossary edit | { draft: { draftSessionId: string, entries: GlossaryEntry[] } } |
| glossary publish | { revision: GlossaryRevision } |
| glossary rollback | { revision: GlossaryRevision } |
| proposal create-spec\|create-glossary | { proposal: DraftProposal } |
| proposal list | { proposals: DraftProposal[] } |
| proposal apply | { mergeOutcome: "Applied", proposal: DraftProposal } |
| proposal discard | { proposal: DraftProposal } |
| authoring-history | { operations: AuthoringOperation[] } |
| capability create | { capability: Capability } |
| capability update | { capability: Capability } |
| capability delete | { deleted: { capabilityId: string } } |
| member list | { members: ProjectMember[] } |
| member directory | { candidates: DirectoryIdentity[] } |
| member add | { member: ProjectMember } |
| member change-role | { member: ProjectMember } |
| member remove | { removed: { userId: string } } |
| token issue | { credential: string, token: ProjectReadToken } |
| token list | { tokens: ProjectReadToken[] } |
| token revoke | { revoked: { tokenId: string } } |
| review request | { review: ManualReviewRecord } |
| review read | { review: ManualReviewRecord } |
| finding list | { findings: ReviewFinding[], nextCursor: string \| null, permissions: FindingPermissions } |
| finding read | { finding: ReviewFinding, permissions: FindingPermissions } |
| finding create | { finding: ReviewFinding, permissions: FindingPermissions } |
| finding confirm\|dismiss\|resolve | { finding: ReviewFinding, permissions: FindingPermissions } |
The named JSON shapes are:
type ProjectSummary = {
effectiveCapabilities: {
canAdministerProject: boolean;
canEditProject: boolean;
canReadProject: true;
};
gherkinLanguage: string;
id: string;
name: string;
role: "Admin" | "Editor" | "Viewer";
status: "Active" | "Archived";
version: string; // opaque ETag
};
type ProjectContext = {
gherkinLanguage: string;
id: string;
name: string;
status: "Active" | "Archived";
version: string; // opaque ETag
};
type GlossaryEntry = {
canonicalTerm: string;
definition: string;
id: string;
usageNote: string | null;
};
type PublishedGlossary = {
entries: GlossaryEntry[];
id: string;
publishedAt: string; // ISO 8601 date-time
};
type Capability = {
id: string;
name: string;
parentId: string | null;
version: string; // opaque ETag, for example: "\"1\""
};
type ProjectMember = {
email: string | null;
issuer: string;
name: string;
role: "Admin" | "Editor" | "Viewer";
subject: string;
userId: string;
version: string; // opaque ETag
};
type DirectoryIdentity = {
email: string | null;
issuer: string;
name: string;
subject: string;
username: string;
};
type Permissions = { canEdit: boolean };
type FindingPermissions = { canWrite: boolean };
type SourceReference = {
externalRevision?: string;
id: string;
section?: string;
title: string;
url: string;
};
type SpecSummary = {
capabilityId: string;
featureName: string;
id: string;
sourceReferences: SourceReference[];
state: "Draft" | "Ready" | "Unavailable";
unavailabilityReason: string | null;
version: string; // opaque ETag
};
type SpecDetails = SpecSummary & {
draftSession: {
documentName: string;
id: string;
textName: "gherkin-source";
} | null;
};
type SpecRevision = {
gherkinSource: string;
id: string;
publishedAt: string; // ISO 8601 date-time
};
type CapabilityPathEntry = { id: string; name: string };
type SearchResult = {
capabilityPath: CapabilityPathEntry[];
spec: SpecSummary;
};
type SourceReferenceInput = Omit<SourceReference, "id">;
type SpecChangelogEntry = {
acceptedAt: string; // ISO 8601 date-time
action:
| "Published"
| "RolledBack"
| "StateChanged"
| "MetadataChanged"
| "FindingCreated"
| "FindingConfirmed"
| "FindingDismissed"
| "FindingResolved"
| "FindingStale";
actorSessionId: string;
actorUserId: string;
changes: {
content: { from: string | null; to: string } | null;
finding: {
findingId: string;
status: "Confirmed" | "Dismissed" | "Proposed" | "Resolved" | "Stale";
} | null;
metadata: {
capabilityId?: { from: string; to: string };
sourceReferences?: {
from: SourceReferenceInput[];
to: SourceReferenceInput[];
};
} | null;
state: {
from: "Draft" | "Ready" | "Unavailable";
fromUnavailabilityReason: string | null;
to: "Draft" | "Ready" | "Unavailable";
toUnavailabilityReason: string | null;
} | null;
};
id: string;
previousRevisionId: string | null;
revisionId: string | null;
};
type GlossaryRevision = {
entries: GlossaryEntry[];
id: string;
publishedAt: string; // ISO 8601 date-time
};
type GlossaryDraftSession = {
documentName: string;
entriesName: "entries";
id: string;
};
type GlossaryDetails = {
currentRevision: GlossaryRevision | null;
draftSession: GlossaryDraftSession;
};
type GlossaryChangelogEntry = {
acceptedAt: string; // ISO 8601 date-time
action: "Published" | "RolledBack";
actorSessionId: string;
actorUserId: string;
changes: {
added: GlossaryEntry[];
removed: GlossaryEntry[];
updated: { from: GlossaryEntry; to: GlossaryEntry }[];
};
id: string;
previousRevisionId: string | null;
revisionId: string;
};
type ProjectReadToken = {
createdAt: string; // ISO 8601 date-time
expiresAt: string | null; // ISO 8601 date-time
id: string;
issuer: { id: string; name: string };
lastUsedAt: string | null; // ISO 8601 date-time
name: string;
revokedAt: string | null; // ISO 8601 date-time
};
type FindingType =
| "Contradiction"
| "Conflict"
| "Duplicate"
| "Ambiguity"
| "Terminology"
| "Unverifiable";
type ManualReviewRecord = {
agentRevision: string;
completedAt: string | null; // ISO 8601 date-time
createdAt: string; // ISO 8601 date-time
draft: { contentHash: string; draftSessionId: string; specId: string };
enabledFindingTypes: FindingType[];
failureCategory:
| "InvalidOutput"
| "ProjectInactive"
| "TerminalExecutionFailure"
| "Timeout"
| null;
findingIds: string[];
glossaryRevisionId: string | null;
id: string;
knownFindingIds: string[];
mastraExecutionVersion: string;
model: string | null;
projectId: string;
requestedBy: { sessionId: string; userId: string };
reviewContractVersion: string;
startedAt: string | null; // ISO 8601 date-time
status:
| "Queued"
| "Running"
| "ProposedFindings"
| "Superseded"
| "Cancelled"
| "Failed";
trigger: "ManualDraft";
updatedAt: string; // ISO 8601 date-time
};
type FindingReference = {
kind: "Feature" | "Rule" | "Background" | "Scenario" | "Examples" | "Step";
text: string;
};
type FindingTarget = {
binding:
| { kind: "Revision"; revisionId: string }
| {
contentHash: string;
draftSessionId: string;
kind: "WorkingDraftSnapshot";
};
references: FindingReference[];
specId: string;
};
type FindingEvidence =
| { incompatibleClaims: [string, string]; kind: "Contradiction" }
| { incompatibleRequirements: [string, string]; kind: "Conflict" }
| { duplicatedBehavior: string; kind: "Duplicate" }
| {
interpretations: [
{ behavior: string; meaning: string },
{ behavior: string; meaning: string },
];
kind: "Ambiguity";
}
| {
glossaryEntryIds: string[];
issue: "MissingDefinition" | "GlossaryConflict" | "MultipleMeanings";
kind: "Terminology";
term: string;
}
| { kind: "Unverifiable"; missingCriterion: string };
type ReviewFinding = {
affectedScenarios: string[];
author: { sessionId: string; userId: string };
conclusion: string;
createdAt: string; // ISO 8601 date-time
evidence: FindingEvidence;
explanation: string;
glossaryReferences: {
entry: GlossaryEntry;
entryId: string;
revisionId: string;
}[];
id: string;
projectId: string;
reviewRecordId: string | null;
status: "Proposed" | "Confirmed" | "Dismissed" | "Resolved" | "Stale";
targets: FindingTarget[];
type: FindingType;
updatedAt: string; // ISO 8601 date-time
version: string; // opaque ETag
};A failed resource command writes nothing to stdout. After any diagnostic lines, the
final stderr line is exactly { "error": { "message": string, "reason":
SkillErrorReason, "retryAfter"?: string, "retryable": boolean,
"diagnostics"?: SkillErrorDiagnostic[] } } where
type SkillErrorDiagnostic = { column?: number; line?: number; message: string;
reason: string }. diagnostics is present whenever the server reports
positioned source problems: Spec Publish and Rollback rejections from the Gherkin
Publish Gate carry one entry per feature-count, language-mismatch,
parse-error, scenario-required, or scenario-id-* reason,
spec assign-scenario-ids carries the same shape when the Working Draft source is
invalid, and other Invalid Operation rejections may carry it too. SkillErrorReason is Authentication Failed,
Project Not Accessible, Permission Denied, Resource Not Found,
Resource Changed, Invalid Operation, or Temporary Failure. Exit status 2
covers authentication, validation, inaccessible/not-found resources, conflicts, and
invalid operations; 3 is permission denial; 4 is a Temporary Failure, whose
retryable field determines whether automatic retry is safe. Exit status 0 is
success. Exit status 1 is reserved for startup/runtime failure before a
resource-command contract can run.
HTTP 429 is a retryable Temporary Failure. When the server supplies
Retry-After, the CLI copies its exact value to retryAfter and does not retry the
request automatically. Generated --help is a separate controlled success path:
top-level help includes authentication and every resource group, nested help includes
the selected group's operations and options, and all help writes to stdout, exits
zero, and does not resolve credentials or call the service.
ProjectSummary.effectiveCapabilities incorporates lifecycle status. Archived
Projects always report canEditProject: false, including for Admins and Editors; an
Archived Admin still reports canAdministerProject: true for Restore and access
reduction. member add is create-only: an existing issuer-and-subject Membership is
an Invalid Operation, and its Role can change only through member change-role with
the version returned by member list. Archived Projects reject Member addition and
Role expansion while still permitting versioned Role reduction and removal.
Every non-idempotent create, Draft-session, Publish, Rollback, Membership mutation,
token issuance, Finding disposition, Manual Review request, and Project lifecycle
command performs at most one fetch-transport retry. It clones the request and reuses
the same Idempotency-Key; an HTTP response, including a response whose body cannot
be decoded, is never dispatched again. Archive and Restore require the opaque Project
version returned by discovery or context; Membership role changes and removals require
the version returned by member list. If neither transport attempt yields a usable
response, the final Temporary Failure has retryable: false and directs the caller
to inspect the resource or Changelog before deciding whether another logical mutation
is appropriate.
Token issuance returns the plaintext credential only for the first committed
response. If a same-key retry confirms the token with credential: null, the CLI
exits nonzero, prints no success payload, and directs the caller to list or revoke the
token before deliberately issuing another.
Source Reference URLs accept only HTTP or HTTPS and must not contain URL userinfo
(username or password). Keep external credentials in the Agent's own authorized
source-reading tool; never embed them in SpecFlow metadata.
The repository-owned official Skill is skills/specflow/SKILL.md. It pins this exact
CLI release, requires explicit User confirmation immediately before Spec or Glossary
Publish and Rollback, and does not send proof of that confirmation to the CLI or API.
The CLI stores its Session credential in macOS Keychain, Windows Credential
Manager, or Linux Secret Service. It does not print or persist the credential in
environment variables. If the system store is unavailable on a POSIX platform,
local-file storage is used only after --allow-file-fallback is explicitly
selected; the CLI warns and enforces directory mode 0700 and file mode 0600.
