Skip to content

Commit 05f8478

Browse files
Support Firebase global active project selection in init
1 parent 153c200 commit 05f8478

10 files changed

Lines changed: 231 additions & 14 deletions

File tree

.changeset/brand-agentpond-init.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"agentpond": patch
3+
---
4+
5+
Show AgentPond branding before the bundled Skills setup flow.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"agentpond": patch
3+
---
4+
5+
Stop Firebase setup when AgentPond skill installation is cancelled or incomplete.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@agentpond/firebase": patch
3+
"agentpond": patch
4+
---
5+
6+
Honor active Firebase CLI project selections even when `.firebaserc` is absent.

apps/cli/src/commands/init.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { spawn } from "node:child_process";
2+
import { existsSync } from "node:fs";
23
import { createRequire } from "node:module";
4+
import { join } from "node:path";
35
import {
46
firebaseCliProjectConfigFromCwd,
57
firebaseProjectDirectory,
@@ -19,6 +21,22 @@ export const AGENTPOND_INIT_SKILLS = [
1921
"agentpond",
2022
] as const;
2123

24+
export function agentPondCliHeader(): string {
25+
return [
26+
"AgentPond",
27+
"Store agent traces remotely. Analyze them locally.",
28+
].join("\n");
29+
}
30+
31+
export function agentPondInitHeader(projectId: string): string {
32+
return [
33+
agentPondCliHeader(),
34+
"",
35+
`Firebase project: ${projectId}`,
36+
"Installing AgentPond skills...",
37+
].join("\n");
38+
}
39+
2240
export const FIREBASE_INSTRUMENTATION_PROMPT = `Use $agentpond-instrumentation to inspect this Firebase project and add
2341
OpenInference tracing to its trusted server-side AI application.
2442
@@ -86,6 +104,8 @@ export function registerInitCommand(
86104
);
87105
}
88106

107+
console.log(agentPondInitHeader(project.projectId));
108+
89109
await (options.installSkills ?? installSkillsWithBundledCli)({
90110
cwd: project.root,
91111
source: AGENTPOND_SKILLS_SOURCE,
@@ -94,7 +114,7 @@ export function registerInitCommand(
94114

95115
console.log(
96116
[
97-
`AgentPond skills installed for Firebase project: ${project.projectId}`,
117+
`AgentPond skills ready for Firebase project: ${project.projectId}`,
98118
"",
99119
"Paste this prompt into your coding agent:",
100120
"",
@@ -126,6 +146,16 @@ export async function installSkillsWithBundledCli(
126146
if (exitCode !== 0) {
127147
throw new CliError(`Skills CLI exited with status ${exitCode}`);
128148
}
149+
150+
const missingSkills = request.skills.filter(
151+
(skill) =>
152+
!existsSync(join(request.cwd, ".agents", "skills", skill, "SKILL.md")),
153+
);
154+
if (missingSkills.length > 0) {
155+
throw new CliError(
156+
`AgentPond skill installation was cancelled or did not complete. Missing: ${missingSkills.join(", ")}`,
157+
);
158+
}
129159
}
130160

131161
async function runSkillsProcess(

apps/cli/tests/cli.test.ts

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
listenOnAvailablePort,
2929
} from "../src/commands/dev.js";
3030
import {
31+
agentPondInitHeader,
3132
FIREBASE_INSTRUMENTATION_PROMPT,
3233
installSkillsWithBundledCli,
3334
MANUAL_SETUP_URL,
@@ -155,7 +156,8 @@ test("CLI init installs AgentPond skills for a nested Firebase project", async (
155156
assert.equal(
156157
output,
157158
[
158-
"AgentPond skills installed for Firebase project: firebase-demo",
159+
agentPondInitHeader("firebase-demo"),
160+
"AgentPond skills ready for Firebase project: firebase-demo",
159161
"",
160162
"Paste this prompt into your coding agent:",
161163
"",
@@ -203,6 +205,51 @@ test("CLI init detects a Firebase project from its root", async () => {
203205
}
204206
});
205207

208+
test("CLI init follows Firebase CLI global active project selections", async () => {
209+
const cwd = process.cwd();
210+
const root = realpathSync(
211+
mkdtempSync(join(tmpdir(), "agentpond-cli-init-firebase-active-")),
212+
);
213+
const configHome = mkdtempSync(
214+
join(tmpdir(), "agentpond-cli-init-firebase-config-"),
215+
);
216+
const originalConfigHome = process.env.XDG_CONFIG_HOME;
217+
const originalExitCode = process.exitCode;
218+
let installCwd: string | undefined;
219+
process.exitCode = undefined;
220+
try {
221+
mkdirSync(join(configHome, "configstore"), { recursive: true });
222+
writeFileSync(join(root, "firebase.json"), "{}", "utf8");
223+
writeFileSync(
224+
join(configHome, "configstore", "firebase-tools.json"),
225+
JSON.stringify({ activeProjects: { [root]: "firebase-active-demo" } }),
226+
"utf8",
227+
);
228+
process.env.XDG_CONFIG_HOME = configHome;
229+
process.chdir(root);
230+
231+
const output = await captureStdout(() =>
232+
main(["node", "agentpond", "init"], {
233+
installSkills: async (request) => {
234+
installCwd = request.cwd;
235+
},
236+
}),
237+
);
238+
239+
assert.equal(process.exitCode, undefined);
240+
assert.equal(installCwd, root);
241+
assert.match(
242+
output,
243+
/AgentPond skills ready for Firebase project: firebase-active-demo/,
244+
);
245+
} finally {
246+
if (originalConfigHome === undefined) delete process.env.XDG_CONFIG_HOME;
247+
else process.env.XDG_CONFIG_HOME = originalConfigHome;
248+
process.chdir(cwd);
249+
process.exitCode = originalExitCode;
250+
}
251+
});
252+
206253
test("CLI init rejects non-Firebase projects without creating files", async () => {
207254
const cwd = process.cwd();
208255
const root = realpathSync(
@@ -346,7 +393,7 @@ test("CLI init does not print a prompt when skill installation fails", async ()
346393
});
347394

348395
assert.equal(process.exitCode, 1);
349-
assert.equal(output, "");
396+
assert.equal(output, [agentPondInitHeader("firebase-demo"), ""].join("\n"));
350397
assert.equal(existsSync(join(root, ".agentpond")), false);
351398
} finally {
352399
process.chdir(cwd);
@@ -355,17 +402,23 @@ test("CLI init does not print a prompt when skill installation fails", async ()
355402
});
356403

357404
test("CLI init invokes the bundled Skills CLI directly without agent flags", async () => {
405+
const root = mkdtempSync(join(tmpdir(), "agentpond-cli-skills-install-"));
358406
let processRequest: SkillsProcessRequest | undefined;
359407
await installSkillsWithBundledCli(
360408
{
361-
cwd: "/firebase/root",
409+
cwd: root,
362410
source: "marcusschiesser/agentpond",
363411
skills: ["agentpond-instrumentation", "agentpond"],
364412
},
365413
{
366414
cliPath: "/package/skills/bin/cli.mjs",
367415
run: async (request) => {
368416
processRequest = request;
417+
for (const skill of ["agentpond-instrumentation", "agentpond"]) {
418+
const skillDir = join(root, ".agents", "skills", skill);
419+
mkdirSync(skillDir, { recursive: true });
420+
writeFileSync(join(skillDir, "SKILL.md"), "---\n---\n", "utf8");
421+
}
369422
return 0;
370423
},
371424
},
@@ -382,12 +435,27 @@ test("CLI init invokes the bundled Skills CLI directly without agent flags", asy
382435
"--skill",
383436
"agentpond",
384437
],
385-
cwd: "/firebase/root",
438+
cwd: root,
386439
});
387440
assert.equal(processRequest?.args.includes("--agent"), false);
388441
assert.equal(processRequest?.args.includes("--yes"), false);
389442
});
390443

444+
test("CLI init treats a cancelled Skills installation as incomplete", async () => {
445+
const root = mkdtempSync(join(tmpdir(), "agentpond-cli-skills-cancelled-"));
446+
447+
await assert.rejects(
448+
installSkillsWithBundledCli(
449+
{ cwd: root, source: "test/source", skills: ["agentpond"] },
450+
{
451+
cliPath: "/package/skills/bin/cli.mjs",
452+
run: async () => 0,
453+
},
454+
),
455+
/installation was cancelled or did not complete.*agentpond/,
456+
);
457+
});
458+
391459
test("CLI trace creation builds a Langfuse-compatible OTEL root span", () => {
392460
const resourceSpans = manualTraceResourceSpans(
393461
{

docs/getting-started/firebase.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,6 @@ Firebase owns project selection. Do not use `npx agentpond env init` or `npx age
5353
## Troubleshooting
5454

5555
- **Firebase is not detected:** Run the command from a directory below the Firebase root and confirm `.firebaserc` or `firebase.json` exists.
56-
- **Project ID is missing:** Run `firebase use <alias-or-project-id>` and retry.
56+
- **Project ID is missing:** Run `firebase use <alias-or-project-id>` and retry. AgentPond follows both `.firebaserc` aliases and the Firebase CLI's global active-project selection.
5757
- **No trusted server runtime exists:** Add or select a server runtime before using the Firebase Admin exporter; never move it into client code.
5858
- **No traces appear:** Confirm the instrumented code path ran, the provider flushed, Firebase Admin selected the default app and bucket, and then rerun `npx agentpond sync`.

packages/firebase/src/firebase-env.ts

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { existsSync, readFileSync } from "node:fs";
2-
import { join } from "node:path";
2+
import { homedir } from "node:os";
3+
import { dirname, join, resolve } from "node:path";
34
import { findAncestorDirectory } from "@agentpond/core";
45

56
export type FirebaseCliProjectConfig = {
@@ -31,12 +32,12 @@ export function firebaseCliProjectConfigFromCwd(
3132

3233
const runtimeConfig = firebaseRuntimeConfig(env.FIREBASE_CONFIG);
3334
const projectId =
34-
firebaseProjectIdFromRc(root) ??
35+
firebaseProjectIdFromCli(root, env) ??
3536
runtimeConfig?.projectId ??
3637
firebaseProjectIdFromEnv(env);
3738
if (!projectId) {
3839
throw new Error(
39-
"Could not determine Firebase project id. Run firebase use <project> to write .firebaserc, or set FIREBASE_CONFIG, GCLOUD_PROJECT, GCP_PROJECT, or GOOGLE_CLOUD_PROJECT.",
40+
"Could not determine Firebase project id. Run firebase use <project> to select a project, or set FIREBASE_CONFIG, GCLOUD_PROJECT, GCP_PROJECT, or GOOGLE_CLOUD_PROJECT.",
4041
);
4142
}
4243

@@ -95,23 +96,64 @@ function firebaseFunctionsSources(functions: unknown): string[] {
9596
return [];
9697
}
9798

98-
function firebaseProjectIdFromRc(root: string): string | undefined {
99+
function firebaseProjectIdFromCli(
100+
root: string,
101+
env: NodeJS.ProcessEnv,
102+
): string | undefined {
103+
const activeProject = firebaseActiveProjectFromConfigStore(root, env);
104+
if (!activeProject) return firebaseProjectIdFromRc(root);
105+
return firebaseProjectIdFromRc(root, activeProject) ?? activeProject;
106+
}
107+
108+
function firebaseProjectIdFromRc(
109+
root: string,
110+
alias = "default",
111+
): string | undefined {
99112
const rcPath = join(root, ".firebaserc");
100113
if (!existsSync(rcPath)) return undefined;
101114
try {
102115
const rcData = JSON.parse(readFileSync(rcPath, "utf8")) as {
103-
projects?: { default?: unknown };
116+
projects?: Record<string, unknown>;
104117
};
105-
const projectId = rcData.projects?.default;
118+
const projectId = rcData.projects?.[alias];
106119
return typeof projectId === "string" && projectId ? projectId : undefined;
107120
} catch (error) {
108121
throw new Error(
109-
"Could not read Firebase project id from .firebaserc projects.default",
122+
`Could not read Firebase project id from .firebaserc projects.${alias}`,
110123
{ cause: error },
111124
);
112125
}
113126
}
114127

128+
function firebaseActiveProjectFromConfigStore(
129+
root: string,
130+
env: NodeJS.ProcessEnv,
131+
): string | undefined {
132+
const configHome =
133+
env.XDG_CONFIG_HOME ||
134+
(env.HOME ? join(env.HOME, ".config") : join(homedir(), ".config"));
135+
const configPath = join(configHome, "configstore", "firebase-tools.json");
136+
if (!existsSync(configPath)) return undefined;
137+
138+
try {
139+
const config = JSON.parse(readFileSync(configPath, "utf8")) as {
140+
activeProjects?: Record<string, unknown>;
141+
};
142+
let currentDir = resolve(root);
143+
while (true) {
144+
const activeProject = config.activeProjects?.[currentDir];
145+
if (typeof activeProject === "string" && activeProject) {
146+
return activeProject;
147+
}
148+
const parentDir = dirname(currentDir);
149+
if (parentDir === currentDir) return undefined;
150+
currentDir = parentDir;
151+
}
152+
} catch {
153+
return undefined;
154+
}
155+
}
156+
115157
function firebaseProjectIdFromEnv(env: NodeJS.ProcessEnv): string | undefined {
116158
return (
117159
firebaseRuntimeConfig(env.FIREBASE_CONFIG)?.projectId ??

packages/firebase/tests/firebase.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,61 @@ test("Firebase CLI project config reads .firebaserc from cwd", () => {
100100
});
101101
});
102102

103+
test("Firebase CLI project config reads global active project selections", () => {
104+
const cwd = mkdtempSync(join(tmpdir(), "agentpond-firebase-active-project-"));
105+
const configHome = mkdtempSync(
106+
join(tmpdir(), "agentpond-firebase-config-home-"),
107+
);
108+
mkdirSync(join(configHome, "configstore"), { recursive: true });
109+
writeFileSync(join(cwd, "firebase.json"), "{}", "utf8");
110+
writeFileSync(
111+
join(configHome, "configstore", "firebase-tools.json"),
112+
JSON.stringify({ activeProjects: { [cwd]: "demo-project" } }),
113+
"utf8",
114+
);
115+
116+
assert.deepEqual(
117+
firebaseCliProjectConfigFromCwd(cwd, {
118+
XDG_CONFIG_HOME: configHome,
119+
} as NodeJS.ProcessEnv),
120+
{
121+
projectId: "demo-project",
122+
root: cwd,
123+
},
124+
);
125+
});
126+
127+
test("Firebase CLI project config resolves globally selected aliases", () => {
128+
const cwd = mkdtempSync(join(tmpdir(), "agentpond-firebase-active-alias-"));
129+
const configHome = mkdtempSync(
130+
join(tmpdir(), "agentpond-firebase-config-home-"),
131+
);
132+
mkdirSync(join(configHome, "configstore"), { recursive: true });
133+
writeFileSync(join(cwd, "firebase.json"), "{}", "utf8");
134+
writeFileSync(
135+
join(cwd, ".firebaserc"),
136+
JSON.stringify({
137+
projects: { default: "prod-project", dev: "dev-project" },
138+
}),
139+
"utf8",
140+
);
141+
writeFileSync(
142+
join(configHome, "configstore", "firebase-tools.json"),
143+
JSON.stringify({ activeProjects: { [cwd]: "dev" } }),
144+
"utf8",
145+
);
146+
147+
assert.deepEqual(
148+
firebaseCliProjectConfigFromCwd(cwd, {
149+
XDG_CONFIG_HOME: configHome,
150+
} as NodeJS.ProcessEnv),
151+
{
152+
projectId: "dev-project",
153+
root: cwd,
154+
},
155+
);
156+
});
157+
103158
test("Firebase CLI project config walks up to Firebase project roots", () => {
104159
const root = mkdtempSync(join(tmpdir(), "agentpond-firebase-monorepo-"));
105160
const cwd = join(root, "packages", "functions");

skills/agentpond/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ firebase use <alias-or-project-id>
2424
npx agentpond sync
2525
```
2626

27+
AgentPond follows the Firebase CLI's active project selection, including
28+
selections stored globally when the project has no `.firebaserc`.
29+
If skill installation is cancelled, `init` stops without printing the coding-agent prompt.
30+
2731
For non-Firebase storage, inspect and select an existing AgentPond environment:
2832

2933
```bash

0 commit comments

Comments
 (0)