Skip to content

Commit 0025615

Browse files
committed
computer: report an unconfigured artifacts binding in help
The disabled artifacts client carries an undefined session id, so the scope-aware help mistook it for a configured client over the whole namespace. Repository and token operations still failed, but their help promised access that did not exist. Give the dispatcher a third help state for a missing binding. Top level, repository, and token help now print the same configuration notice as failed operations instead of describing either session or namespace access. Direct artifacts clients keep their existing two states.
1 parent 829b238 commit 0025615

5 files changed

Lines changed: 85 additions & 46 deletions

File tree

docs/15_artifacts_interface.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,11 @@ shared link does not disturb others.
256256

257257
Help is a first-class, agent-readable surface. `help`, `--help`,
258258
`-h`, and each group's `--help` print documentation that spells out
259-
the secret-handling rules and whichever naming contract the client
260-
actually applies: local names under a session, stored names and a
261-
namespace-wide `repo list` without one. A bare `artifacts` prints
262-
the top-level help and exits non-zero, the way `git` with no args
263-
does.
259+
the secret-handling rules and the client's actual state: local names
260+
under a session, stored names and a namespace-wide `repo list`
261+
without one, or a clear configuration notice when no binding is
262+
available. A bare `artifacts` prints the top-level help and exits
263+
non-zero, the way `git` with no args does.
264264

265265
### Exit codes
266266

@@ -322,7 +322,8 @@ quietly widening that tenant's access.
322322

323323
When `artifacts` is omitted from `Workspace`, the command still
324324
exists, but operations fail with a clear "Workspace Artifacts binding
325-
is not configured" error.
325+
is not configured" error. Its help prints the same notice instead of
326+
describing session or namespace access that is not available.
326327

327328
The binding stanza in the consumer's Wrangler config:
328329

packages/computer/src/artifacts/cli.ts

Lines changed: 58 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import type { ArtifactClient } from "./client.js";
1717
import { parseDuration } from "./duration.js";
18-
import { ArtifactError } from "./errors.js";
18+
import { ARTIFACTS_NOT_CONFIGURED_MESSAGE, ArtifactError } from "./errors.js";
1919
import type { ArtifactScope } from "./types.js";
2020

2121
/**
@@ -58,31 +58,35 @@ export interface ArtifactsCLIResult {
5858
export async function runArtifactsCLI(
5959
client: ArtifactClient,
6060
input: ArtifactsCLIInput,
61+
bindingConfigured = true,
6162
): Promise<ArtifactsCLIResult> {
6263
const argv = input.argv;
63-
// Help describes the scoping the client actually applies, so every
64-
// help path is told whether this client sits in one session or
65-
// spans the namespace.
66-
const scoped = client.sessionId !== undefined;
64+
// Help describes the state the client actually runs in: one
65+
// session, the whole namespace, or no binding at all.
66+
const mode: ArtifactsHelpMode = bindingConfigured
67+
? client.sessionId === undefined
68+
? "namespace"
69+
: "session"
70+
: "unconfigured";
6771
if (argv.length === 0) {
6872
// Bare invocation: print help but signal misuse, the way `git`
6973
// with no args exits non-zero.
70-
return { stdout: topLevelHelp(scoped), stderr: "", exitCode: 1 };
74+
return { stdout: topLevelHelp(mode), stderr: "", exitCode: 1 };
7175
}
7276
const [group, ...rest] = argv;
7377
switch (group) {
7478
case "help":
7579
case "--help":
7680
case "-h":
77-
return ok(topLevelHelp(scoped));
81+
return ok(topLevelHelp(mode));
7882
case "create":
7983
return await runCreate(client, rest, input.remoteAdd);
8084
case "share":
8185
return await runShare(client, rest);
8286
case "repo":
83-
return await runRepo(client, rest, scoped);
87+
return await runRepo(client, rest, mode);
8488
case "token":
85-
return await runToken(client, rest, scoped);
89+
return await runToken(client, rest, mode);
8690
default:
8791
return fail(`artifacts: '${group}' is not an artifacts command. See 'artifacts help'.`);
8892
}
@@ -322,17 +326,17 @@ export function credentialURL(remote: string, token: string): string {
322326
async function runRepo(
323327
client: ArtifactClient,
324328
args: string[],
325-
scoped: boolean,
329+
mode: ArtifactsHelpMode,
326330
): Promise<ArtifactsCLIResult> {
327331
if (args.length === 0) {
328-
return { stdout: repoHelp(scoped), stderr: "", exitCode: 1 };
332+
return { stdout: repoHelp(mode), stderr: "", exitCode: 1 };
329333
}
330334
const [sub, ...rest] = args;
331335
switch (sub) {
332336
case "help":
333337
case "--help":
334338
case "-h":
335-
return ok(repoHelp(scoped));
339+
return ok(repoHelp(mode));
336340
case "create":
337341
return await runRepoCreate(client, rest);
338342
case "get":
@@ -472,17 +476,17 @@ async function runRepoImport(client: ArtifactClient, args: string[]): Promise<Ar
472476
async function runToken(
473477
client: ArtifactClient,
474478
args: string[],
475-
scoped: boolean,
479+
mode: ArtifactsHelpMode,
476480
): Promise<ArtifactsCLIResult> {
477481
if (args.length === 0) {
478-
return { stdout: tokenHelp(scoped), stderr: "", exitCode: 1 };
482+
return { stdout: tokenHelp(mode), stderr: "", exitCode: 1 };
479483
}
480484
const [sub, ...rest] = args;
481485
switch (sub) {
482486
case "help":
483487
case "--help":
484488
case "-h":
485-
return ok(tokenHelp(scoped));
489+
return ok(tokenHelp(mode));
486490
case "create":
487491
return await runTokenCreate(client, rest);
488492
case "list":
@@ -591,11 +595,14 @@ async function runTokenDelete(client: ArtifactClient, args: string[]): Promise<A
591595
// help text
592596
// ---------------------------------------------------------------
593597
//
594-
// The two name paragraphs are the only thing scoping changes. A
595-
// client bound to a session works in local names; one bound to the
596-
// namespace works in stored names and sees every session's
597-
// repositories. Help states whichever is true, because a consumer
598-
// that believes the wrong one addresses the wrong repository.
598+
// The naming paragraph follows the client state. A client bound to
599+
// a session works in local names; one bound to the namespace works
600+
// in stored names and sees every session's repositories; one with
601+
// no binding has neither authority. Help states whichever is true,
602+
// because a consumer that believes the wrong one addresses the
603+
// wrong repository.
604+
605+
type ArtifactsHelpMode = "session" | "namespace" | "unconfigured";
599606

600607
const SCOPED_NAMES = `Every repository name you pass is implicitly scoped to this
601608
session: the name 'starter' addresses the repo stored as
@@ -611,12 +618,28 @@ including those a session owns (stored as '<session>__<name>'), and
611618
any of them can be read, written to, or deleted under that name.
612619
`;
613620

614-
function topLevelHelp(scoped: boolean): string {
621+
const UNCONFIGURED_NAMES = `${ARTIFACTS_NOT_CONFIGURED_MESSAGE}.
622+
Configure WorkspaceOptions.artifacts before running repository or
623+
token commands.
624+
`;
625+
626+
function namingHelp(mode: ArtifactsHelpMode): string {
627+
switch (mode) {
628+
case "session":
629+
return SCOPED_NAMES;
630+
case "namespace":
631+
return UNSCOPED_NAMES;
632+
case "unconfigured":
633+
return UNCONFIGURED_NAMES;
634+
}
635+
}
636+
637+
function topLevelHelp(mode: ArtifactsHelpMode): string {
615638
return `usage: artifacts <command> [<args>]
616639
617640
Manage Cloudflare Artifacts repositories and git tokens.
618641
619-
${scoped ? SCOPED_NAMES : UNSCOPED_NAMES}
642+
${namingHelp(mode)}
620643
Commands:
621644
create Create a repo, mint a token, and register a git remote.
622645
share Mint a token and print one clone-ready remote URL.
@@ -660,15 +683,18 @@ Secrets: 'create', 'share', and 'token create' all surface a token
660683
`;
661684
}
662685

663-
function repoHelp(scoped: boolean): string {
664-
const nameKind = scoped ? "a local name" : "the stored name";
686+
function repoHelp(mode: ArtifactsHelpMode): string {
687+
const nameKind =
688+
mode === "session" ? "a local name" : mode === "namespace" ? "the stored name" : "its name";
689+
const listScope =
690+
mode === "session"
691+
? "this session's repository"
692+
: mode === "namespace"
693+
? "the namespace's repository"
694+
: "repository";
665695
return `usage: artifacts repo <subcommand> [<args>]
666696
667-
${
668-
scoped
669-
? "All names are session-scoped; pass local names only."
670-
: "Names are namespace-wide; pass the name a repository is stored under."
671-
}
697+
${namingHelp(mode)}
672698
673699
repo create <name> [--description <text>] [--default-branch <branch>] [--read-only]
674700
Create a repository. Prints JSON: { name, remote, defaultBranch, token }.
@@ -678,7 +704,7 @@ ${
678704
Print JSON metadata for a repository: ArtifactsRepoInfo with ${nameKind}.
679705
680706
repo list
681-
Print a JSON array of ${scoped ? "this session's" : "the namespace's"} repository metadata (without remote).
707+
Print a JSON array of ${listScope} metadata (without remote).
682708
683709
repo delete <name>
684710
Delete a repository. Prints a one-line confirmation.
@@ -692,15 +718,11 @@ Example:
692718
`;
693719
}
694720

695-
function tokenHelp(scoped: boolean): string {
721+
function tokenHelp(mode: ArtifactsHelpMode): string {
696722
return `usage: artifacts token <subcommand> [<args>]
697723
698724
Tokens authenticate git operations against a repository's remote.
699-
${
700-
scoped
701-
? "The <repo> argument is a session-scoped local name."
702-
: "The <repo> argument is the name a repository is stored under."
703-
}
725+
${namingHelp(mode)}
704726
705727
token create <repo> [--scope read|write] [--ttl <dur>]
706728
Mint a token. Prints JSON: { id, plaintext, scope, expiresAt }.

packages/computer/src/artifacts/errors.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Typed error hierarchy for the artifacts surface.
2-
//
2+
3+
export const ARTIFACTS_NOT_CONFIGURED_MESSAGE = "Workspace Artifacts binding is not configured";
4+
35
// The CLI dispatcher in `cli.ts` maps each class to a
46
// deterministic exit code and a stderr line so a shell consumer
57
// can match on the text. The hierarchy is deliberately narrow:

packages/computer/src/workspace.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,20 @@ describe("Workspace backend selection", () => {
770770
expect(res.stderr).toContain("Workspace Artifacts binding is not configured");
771771
});
772772

773+
it("reports the missing binding in help instead of promising namespace access", async () => {
774+
const ws = new Workspace({ storage: makeStorage() });
775+
const top = await ws.artifacts.cli({ argv: ["help"] });
776+
const repo = await ws.artifacts.cli({ argv: ["repo", "--help"] });
777+
const token = await ws.artifacts.cli({ argv: ["token", "--help"] });
778+
779+
for (const result of [top, repo, token]) {
780+
expect(result.exitCode).toBe(0);
781+
expect(result.stdout).toContain("Workspace Artifacts binding is not configured");
782+
expect(result.stdout).not.toContain("every repository in the namespace");
783+
expect(result.stdout).not.toContain("namespace-wide");
784+
}
785+
});
786+
773787
it("scopes to the workspace session id", async () => {
774788
const binding = new FakeArtifactsBinding();
775789
const ws = new Workspace({

packages/computer/src/workspace.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
SQLiteWorkspaceProvider,
2020
WorkspaceFilesystem,
2121
} from "@cloudflare/dofs";
22-
22+
import { ARTIFACTS_NOT_CONFIGURED_MESSAGE } from "./artifacts/errors.js";
2323
import {
2424
type ArtifactClient,
2525
ArtifactError,
@@ -1356,7 +1356,7 @@ function artifactsSessionId(options: WorkspaceOptions): string | null | undefine
13561356

13571357
function createDisabledArtifactsClient(): ArtifactClient {
13581358
const fail = () => {
1359-
throw new ArtifactError("ENOCONFIG", "Workspace Artifacts binding is not configured");
1359+
throw new ArtifactError("ENOCONFIG", ARTIFACTS_NOT_CONFIGURED_MESSAGE);
13601360
};
13611361
return {
13621362
sessionId: undefined,
@@ -1370,7 +1370,7 @@ function createDisabledArtifactsClient(): ArtifactClient {
13701370
getToken: fail,
13711371
revokeToken: fail,
13721372
async cli(input) {
1373-
return runArtifactsCLI(this, input);
1373+
return runArtifactsCLI(this, input, false);
13741374
},
13751375
} as ArtifactClient;
13761376
}

0 commit comments

Comments
 (0)