@bastani/atomic 0.5.28-0 → 0.5.28-2
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/.claude/settings.json +0 -1
- package/.opencode/opencode.json +7 -2
- package/dist/commands/cli/claude-stop-hook.d.ts +12 -1
- package/dist/commands/cli/claude-stop-hook.d.ts.map +1 -1
- package/dist/sdk/providers/claude.d.ts +20 -0
- package/dist/sdk/providers/claude.d.ts.map +1 -1
- package/dist/sdk/providers/copilot.d.ts +17 -0
- package/dist/sdk/providers/copilot.d.ts.map +1 -1
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/services/config/definitions.d.ts +7 -0
- package/dist/services/config/definitions.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/commands/cli/chat/index.ts +11 -0
- package/src/commands/cli/claude-stop-hook.test.ts +51 -5
- package/src/commands/cli/claude-stop-hook.ts +216 -13
- package/src/commands/cli/init/onboarding.ts +6 -1
- package/src/commands/cli/workflow-command.test.ts +40 -0
- package/src/commands/cli/workflow.ts +10 -0
- package/src/lib/merge.ts +29 -4
- package/src/sdk/providers/claude.ts +80 -0
- package/src/sdk/providers/copilot.ts +20 -0
- package/src/sdk/providers/headless-hil-policy.test.ts +41 -1
- package/src/sdk/runtime/executor.ts +10 -1
- package/src/services/config/definitions.ts +10 -0
- package/src/services/system/auth.test.ts +194 -0
- package/src/services/system/auth.ts +137 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fail-fast login checks for the agent CLIs atomic drives.
|
|
3
|
+
*
|
|
4
|
+
* Copilot exposes `CopilotClient.getAuthStatus()` — a thin JSON-RPC call
|
|
5
|
+
* that returns `isAuthenticated: boolean`. Claude Agent SDK has no
|
|
6
|
+
* direct "is signed in" primitive, but `query().initializationResult()`
|
|
7
|
+
* returns an `account` record populated with `email`, `tokenSource`,
|
|
8
|
+
* and/or `apiKeySource` whenever the CLI has valid credentials — an
|
|
9
|
+
* empty record means the user never completed the OAuth flow and no
|
|
10
|
+
* API key is in the environment.
|
|
11
|
+
*
|
|
12
|
+
* We run these probes BEFORE spawning the native CLI from `atomic chat`
|
|
13
|
+
* or `atomic workflow` so the user sees a short actionable error
|
|
14
|
+
* instead of dropping into an interactive agent that immediately
|
|
15
|
+
* prompts for login (or, in the workflow case, silently stalls).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { AgentKey } from "../config/index.ts";
|
|
19
|
+
import { COLORS } from "../../theme/colors.ts";
|
|
20
|
+
import { copilotSubprocessEnv } from "../../sdk/providers/copilot.ts";
|
|
21
|
+
|
|
22
|
+
export interface AuthCheckResult {
|
|
23
|
+
/** True when the SDK reports the user is authenticated. */
|
|
24
|
+
loggedIn: boolean;
|
|
25
|
+
/** Optional human-readable detail — usually the SDK's status message. */
|
|
26
|
+
detail?: string;
|
|
27
|
+
/** Login identity if reported (GitHub login for Copilot, email for Claude). */
|
|
28
|
+
identity?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Verify the user is authenticated for the given agent. For agents that
|
|
33
|
+
* do not expose an SDK-level auth probe (currently `opencode`), returns
|
|
34
|
+
* `{ loggedIn: true }` so the caller can skip the check.
|
|
35
|
+
*/
|
|
36
|
+
export async function checkAgentAuth(agent: AgentKey): Promise<AuthCheckResult> {
|
|
37
|
+
if (agent === "copilot") return checkCopilotAuth();
|
|
38
|
+
if (agent === "claude") return checkClaudeAuth();
|
|
39
|
+
return { loggedIn: true };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Print a consistent login-required banner to stderr. Exported so
|
|
44
|
+
* `chatCommand` and `workflowCommand` share the same wording.
|
|
45
|
+
*/
|
|
46
|
+
export function printAuthError(agent: AgentKey, result: AuthCheckResult): void {
|
|
47
|
+
const { name, loginHint } = AUTH_PROMPTS[agent];
|
|
48
|
+
console.error(
|
|
49
|
+
`${COLORS.red}Error: Not logged in to ${name}.${COLORS.reset}`,
|
|
50
|
+
);
|
|
51
|
+
if (result.detail) {
|
|
52
|
+
console.error(`${COLORS.dim}${result.detail}${COLORS.reset}`);
|
|
53
|
+
}
|
|
54
|
+
console.error(loginHint);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const AUTH_PROMPTS: Record<AgentKey, { name: string; loginHint: string }> = {
|
|
58
|
+
claude: {
|
|
59
|
+
name: "Claude Code",
|
|
60
|
+
loginHint:
|
|
61
|
+
"Run `claude` and complete the /login flow (or set ANTHROPIC_API_KEY), then retry.",
|
|
62
|
+
},
|
|
63
|
+
copilot: {
|
|
64
|
+
name: "GitHub Copilot CLI",
|
|
65
|
+
loginHint: "Run `copilot` and complete the `/login` flow, then retry.",
|
|
66
|
+
},
|
|
67
|
+
opencode: {
|
|
68
|
+
name: "OpenCode",
|
|
69
|
+
loginHint: "Run `opencode auth login`, then retry.",
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
async function checkCopilotAuth(): Promise<AuthCheckResult> {
|
|
74
|
+
const { CopilotClient } = await import("@github/copilot-sdk");
|
|
75
|
+
const client = new CopilotClient({ env: copilotSubprocessEnv() });
|
|
76
|
+
try {
|
|
77
|
+
await client.start();
|
|
78
|
+
const status = await client.getAuthStatus();
|
|
79
|
+
return {
|
|
80
|
+
loggedIn: status.isAuthenticated,
|
|
81
|
+
detail: status.statusMessage,
|
|
82
|
+
identity: status.login,
|
|
83
|
+
};
|
|
84
|
+
} catch (err) {
|
|
85
|
+
return {
|
|
86
|
+
loggedIn: false,
|
|
87
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
88
|
+
};
|
|
89
|
+
} finally {
|
|
90
|
+
try {
|
|
91
|
+
await client.stop();
|
|
92
|
+
} catch {
|
|
93
|
+
// Best effort — a failed stop shouldn't shadow the probe result.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function checkClaudeAuth(): Promise<AuthCheckResult> {
|
|
99
|
+
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
100
|
+
const { resolveHeadlessClaudeBin } = await import("../../sdk/providers/claude.ts");
|
|
101
|
+
|
|
102
|
+
// A never-yielding iterable keeps the query idle while we probe the
|
|
103
|
+
// initialization result. The SDK starts the `claude` subprocess on
|
|
104
|
+
// query construction but only blocks on the stream once a prompt is
|
|
105
|
+
// actually delivered.
|
|
106
|
+
async function* emptyStream(): AsyncGenerator<never, void, void> {}
|
|
107
|
+
|
|
108
|
+
const q = query({
|
|
109
|
+
prompt: emptyStream(),
|
|
110
|
+
options: {
|
|
111
|
+
pathToClaudeCodeExecutable: resolveHeadlessClaudeBin(),
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const init = await q.initializationResult();
|
|
117
|
+
const account = init.account ?? {};
|
|
118
|
+
const loggedIn = Boolean(
|
|
119
|
+
account.email || account.tokenSource || account.apiKeySource,
|
|
120
|
+
);
|
|
121
|
+
return {
|
|
122
|
+
loggedIn,
|
|
123
|
+
identity: account.email,
|
|
124
|
+
};
|
|
125
|
+
} catch (err) {
|
|
126
|
+
return {
|
|
127
|
+
loggedIn: false,
|
|
128
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
129
|
+
};
|
|
130
|
+
} finally {
|
|
131
|
+
try {
|
|
132
|
+
q.close();
|
|
133
|
+
} catch {
|
|
134
|
+
// Best effort — the subprocess is torn down on process exit anyway.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|