Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type { PluginPick } from "../../../../presentation/prompts/plugin-pick-us
import type { MarketplaceTrustStore } from "../../../distribution/domain/ports/marketplace-trust-store.js";
import { assertToolSupportsScope, type InstallScope } from "../../domain/install-scope.js";
import { parsePluginSpec } from "../../domain/plugins/installed-plugin.js";
import type { Environment } from "../../domain/ports/environment.js";
import type { ManifestRepository } from "../../domain/ports/manifest-repository.js";
import type { PluginAdd } from "./plugin-add-use-case.js";
import type { PluginInstallFromMarketplace } from "./plugin-install-from-marketplace-use-case.js";
Expand All @@ -22,7 +21,6 @@ export interface PluginInstallOptions {
projectRoot: string;
interactive: boolean;
fromMarketplace?: string;
token?: string;
yes?: boolean;
scope?: InstallScope;
}
Expand All @@ -39,8 +37,7 @@ export class PluginInstallUseCase {
private readonly pluginInstallFromMarketplaceUseCase: PluginInstallFromMarketplace,
private readonly manifestRepo: ManifestRepository,
private readonly trustStore: MarketplaceTrustStore,
private readonly prompter: Prompter,
private readonly environment: Environment
private readonly prompter: Prompter
) {}

async execute(options: PluginInstallOptions): Promise<PluginInstallResult> {
Expand Down Expand Up @@ -109,7 +106,6 @@ export class PluginInstallUseCase {

private async executeMarketplace(options: PluginInstallOptions): Promise<PluginInstallResult> {
const { name, version } = parsePluginSpec(options.pluginArg as string);
if (options.token) this.environment.set("AIDD_TOKEN", options.token);
const result = await this.pluginInstallFromMarketplaceUseCase.execute({
pluginName: name,
version,
Expand Down
4 changes: 1 addition & 3 deletions cli/src/contexts/framework/domain/ports/environment.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
/** The ambient environment a use case reads a switch from, and publishes a token to. A port, so
* neither layer reaches a global: the composition root supplies what owns `process.env`. */
/** The environment a use case reads a switch from; the composition root owns `process.env`. */
export interface Environment {
get(name: string): string | undefined;
set(name: string, value: string): void;
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import type { Environment } from "../domain/ports/environment.js";

/** Reads and writes at call time, never snapshotting at construction: an e2e run sets its
* switches in the child process it spawns, after this adapter exists. */
/** Reads at call time, never snapshotting at construction: an e2e run sets its switches in
* the child process it spawns, after this adapter exists. */
export class EnvironmentAdapter implements Environment {
get(name: string): string | undefined {
return process.env[name];
}

set(name: string, value: string): void {
process.env[name] = value;
}
}
3 changes: 1 addition & 2 deletions cli/src/presentation/commands/marketplace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,8 @@ export function registerMarketplaceCommand(program: Command): void {
process.exit(1);
}
try {
if (cmdOptions.token) process.env.AIDD_TOKEN = cmdOptions.token;
const scope: MarketplaceScope = cmdOptions.scope === "user" ? "user" : "project";
const deps = await createDeps(projectRoot, { verbose }, output);
const deps = await createDeps(projectRoot, { verbose, token: cmdOptions.token }, output);
const name = nameArg ?? (await deps.prompter.input("Marketplace name:"));
const rawSource = sourceArg ?? (await deps.prompter.input("Source (path or user/repo):"));
const source = parsePluginSourceShorthand(rawSource);
Expand Down
3 changes: 1 addition & 2 deletions cli/src/presentation/commands/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,13 @@ export function registerPluginCommand(program: Command): void {
try {
assertValidAiToolId(cmdOptions.tool);
const scope = parseInstallScope(cmdOptions.scope);
const deps = await createDeps(projectRoot, { verbose }, output);
const deps = await createDeps(projectRoot, { verbose, token: cmdOptions.token }, output);
const result = await deps.pluginInstallUseCase.execute({
pluginArg,
toolIds: parseToolOption(cmdOptions.tool),
projectRoot,
interactive: process.stdout.isTTY,
fromMarketplace: cmdOptions.from,
token: cmdOptions.token,
yes: cmdOptions.yes,
scope,
});
Expand Down
8 changes: 7 additions & 1 deletion cli/src/runtime/auth/auth-reader-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ export class AuthReaderAdapter implements TokenProvider {
private readonly storage: AuthStorage,
private readonly projectRoot: string,
private readonly logger?: Logger,
private readonly externalProvider: TokenResolver = noopExternalProvider
private readonly externalProvider: TokenResolver = noopExternalProvider,
/** A token the command line carried: composed in, never published to the environment. */
private readonly explicitToken?: string
) {}

resolve(): Promise<string | null> {
Expand All @@ -28,6 +30,10 @@ export class AuthReaderAdapter implements TokenProvider {
}

private async resolveUncached(): Promise<string | null> {
if (this.explicitToken) {
this.logger?.debug("Token given on the command line");
return this.explicitToken;
}
const envToken = process.env.AIDD_TOKEN;
if (envToken) {
this.logger?.debug("Token resolved from AIDD_TOKEN env");
Expand Down
17 changes: 12 additions & 5 deletions cli/src/runtime/wiring/framework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ import { createFrameworkBuildUseCase } from "./translate.js";

interface GlobalOptions {
verbose: boolean;
token?: string;
}

interface Deps extends TelemetryDeps {
Expand Down Expand Up @@ -182,7 +183,8 @@ export async function createDeps(
options: GlobalOptions,
output?: CLIOutput
): Promise<Deps> {
const cached = _cache.get(projectRoot);
const cacheKey = `${projectRoot}\u0000${options.token ?? ""}`;
const cached = _cache.get(cacheKey);
if (cached !== undefined) return cached;
const hasher = new HasherAdapter();
const logger = output ?? new CLIOutput(options.verbose);
Expand All @@ -193,7 +195,13 @@ export async function createDeps(
const http = new HttpClient();
const authStorage = new AuthStorage();
const ghCliAdapter = new GhCliAdapter();
const authReader = new AuthReaderAdapter(authStorage, projectRoot, logger, ghCliAdapter);
const authReader = new AuthReaderAdapter(
authStorage,
projectRoot,
logger,
ghCliAdapter,
options.token
);
const credentialStore = new AuthProviderAdapter(
authStorage,
new Map([["gh", ghCliAdapter]]),
Expand Down Expand Up @@ -358,8 +366,7 @@ export async function createDeps(
pluginInstallFromMarketplaceUseCase,
manifestRepo,
marketplaceTrustStore,
prompter,
environment
prompter
);
const installAiToolUseCase = new InstallAiToolUseCase(
installRuntimeConfigUseCase,
Expand Down Expand Up @@ -575,6 +582,6 @@ export async function createDeps(
listInstalledRulesUseCase,
checkUpdateUseCase,
};
_cache.set(projectRoot, deps);
_cache.set(cacheKey, deps);
return deps;
}
2 changes: 1 addition & 1 deletion cli/tests/architecture/orchestrator-deps.arch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const BASELINE: readonly { readonly path: string; readonly injected: number }[]
{ path: "src/contexts/telemetry/application/read-local-cost-use-case.ts", injected: 7 },
{ path: "src/contexts/telemetry/application/report-cost-use-case.ts", injected: 7 },
{ path: "src/contexts/framework/application/install/install-ide-tool-use-case.ts", injected: 6 },
{ path: "src/contexts/framework/application/plugin/plugin-install-use-case.ts", injected: 7 },
{ path: "src/contexts/framework/application/plugin/plugin-install-use-case.ts", injected: 6 },
{ path: "src/contexts/framework/application/plugin/plugin-update-use-case.ts", injected: 6 },
{
path: "src/contexts/framework/application/restore/restore-all-plugins-use-case.ts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
} from "../../../../../src/kernel/errors.js";
import type { Prompter } from "../../../../../src/kernel/ports/prompter.js";
import type { PluginPick } from "../../../../../src/presentation/prompts/plugin-pick-use-case.js";
import { InMemoryEnvironment } from "../../../../helpers/ports/in-memory-environment.js";
import { InMemoryManifestRepository } from "../../../../helpers/ports/in-memory-manifest-repository.js";

const PLUGIN_FIXTURE = join(process.cwd(), "tests/fixtures/plugins/claude-format/sample-plugin");
Expand Down Expand Up @@ -43,7 +42,6 @@ function makeUseCases(overrides?: {
marketplaceExecute?: ReturnType<typeof vi.fn>;
trustStore?: MarketplaceTrustStore;
prompter?: Prompter;
environment?: InMemoryEnvironment;
}) {
const pickExecute = overrides?.pickExecute ?? vi.fn();
const addExecute = overrides?.addExecute ?? vi.fn();
Expand All @@ -56,15 +54,13 @@ function makeUseCases(overrides?: {
const manifestRepo = new InMemoryManifestRepository();
const trustStore = overrides?.trustStore ?? makeAlwaysTrustStore();
const prompter = overrides?.prompter ?? makeSilentPrompter();
const environment = overrides?.environment ?? new InMemoryEnvironment();
return {
pluginPickUseCase,
pluginAddUseCase,
pluginInstallFromMarketplaceUseCase,
manifestRepo,
trustStore,
prompter,
environment,
pickExecute,
addExecute,
marketplaceExecute,
Expand All @@ -79,16 +75,14 @@ function makeUseCase(overrides?: Parameters<typeof makeUseCases>[0]): PluginInst
manifestRepo,
trustStore,
prompter,
environment,
} = makeUseCases(overrides);
return new PluginInstallUseCase(
pluginPickUseCase,
pluginAddUseCase,
pluginInstallFromMarketplaceUseCase,
manifestRepo,
trustStore,
prompter,
environment
prompter
);
}

Expand Down Expand Up @@ -284,35 +278,4 @@ describe("PluginInstallUseCase", () => {
expect(trustStore.isTrusted).not.toHaveBeenCalled();
});
});

describe("token publication", () => {
it("publishes --token through the environment, for a fetcher built before the flag arrived", async () => {
const environment = new InMemoryEnvironment();
const marketplaceExecute = vi.fn().mockResolvedValue({ entry: { name: "my-plugin" } });

await makeUseCase({ marketplaceExecute, environment }).execute({
pluginArg: "my-plugin",
toolIds: "all",
projectRoot: PROJECT_ROOT,
interactive: false,
token: "ghp_from_flag",
});

expect(environment.get("AIDD_TOKEN")).toBe("ghp_from_flag");
});

it("publishes nothing when no token is passed", async () => {
const environment = new InMemoryEnvironment();
const marketplaceExecute = vi.fn().mockResolvedValue({ entry: { name: "my-plugin" } });

await makeUseCase({ marketplaceExecute, environment }).execute({
pluginArg: "my-plugin",
toolIds: "all",
projectRoot: PROJECT_ROOT,
interactive: false,
});

expect(environment.get("AIDD_TOKEN")).toBeUndefined();
});
});
});
4 changes: 0 additions & 4 deletions cli/tests/helpers/ports/in-memory-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,4 @@ export class InMemoryEnvironment implements Environment {
get(name: string): string | undefined {
return this.values.get(name);
}

set(name: string, value: string): void {
this.values.set(name, value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ const PROJECT_ROOT = process.cwd();

let written: string[] = [];
let errors: string[] = [];
let tokenBefore: string | undefined;

function pretendTerminal(isTTY: boolean): void {
Object.defineProperty(process.stdout, "isTTY", { value: isTTY, configurable: true });
Expand All @@ -48,7 +47,6 @@ beforeEach(() => {
vi.clearAllMocks();
written = [];
errors = [];
tokenBefore = process.env.AIDD_TOKEN;
pretendTerminal(false);
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
written.push(String(chunk));
Expand Down Expand Up @@ -77,8 +75,6 @@ beforeEach(() => {

afterEach(() => {
vi.restoreAllMocks();
if (tokenBefore === undefined) delete process.env.AIDD_TOKEN;
else process.env.AIDD_TOKEN = tokenBefore;
process.exitCode = undefined;
});

Expand Down Expand Up @@ -148,10 +144,15 @@ describe("aidd marketplace add", () => {
);
});

it("puts the given token where the fetch will read it", async () => {
it("hands the given token to the composition root, where every fetcher reads it", async () => {
await run("add", "market-b", "/some/source", "--token", "ghp_x");

expect(process.env.AIDD_TOKEN).toBe("ghp_x");
expect(vi.mocked(createDeps)).toHaveBeenCalledWith(
PROJECT_ROOT,
{ verbose: false, token: "ghp_x" },
expect.anything()
);
expect(process.env.AIDD_TOKEN).not.toBe("ghp_x");
});

it("asks for the name and the source it was not given, on a terminal", async () => {
Expand Down Expand Up @@ -192,12 +193,14 @@ describe("aidd marketplace add", () => {
});
});

it("leaves the token environment alone when none was given", async () => {
delete process.env.AIDD_TOKEN;

it("hands no token when none was given", async () => {
await run("add", "market-b", "/some/source");

expect(process.env.AIDD_TOKEN).toBeUndefined();
expect(vi.mocked(createDeps)).toHaveBeenCalledWith(
PROJECT_ROOT,
{ verbose: false, token: undefined },
expect.anything()
);
});

it("names a failed registration on stderr and fails the process", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ describe("aidd plugin install", () => {
projectRoot: PROJECT_ROOT,
interactive: false,
fromMarketplace: undefined,
token: undefined,
yes: undefined,
scope: undefined,
});
Expand All @@ -179,6 +178,17 @@ describe("aidd plugin install", () => {
});
});

it("hands --token to the composition root, where every fetcher reads it, and to nothing else", async () => {
await run("install", "aidd-dev", "--token", "ghp_x");

expect(vi.mocked(createDeps)).toHaveBeenCalledWith(
PROJECT_ROOT,
{ verbose: false, token: "ghp_x" },
expect.anything()
);
expect(pluginInstall.mock.calls[0][0]).not.toHaveProperty("token");
});

it("narrows activation to the marketplace the install was told to use", async () => {
await run("install", "aidd-dev", "--from", "market-b");

Expand All @@ -191,7 +201,7 @@ describe("aidd plugin install", () => {
});
});

it("carries the scope, the token and the auto-answer a scripted run gave", async () => {
it("carries the scope and the auto-answer a scripted run gave", async () => {
await run(
"install",
"aidd-dev",
Expand All @@ -205,7 +215,7 @@ describe("aidd plugin install", () => {
);

expect(pluginInstall).toHaveBeenCalledWith(
expect.objectContaining({ scope: "user", token: "ghp_x", yes: true, toolIds: ["claude"] })
expect.objectContaining({ scope: "user", yes: true, toolIds: ["claude"] })
);
});

Expand Down
Loading