Skip to content
Open
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
11 changes: 9 additions & 2 deletions UPSTREAM_DIFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,16 @@
|-------|--------|
| `api.projects.:id.spectrum.avatar-upload-url` | `GET` |

## Changed Routes

| Route | Method | Change |
|-------|--------|--------|
| `api.projects.:id.spectrum.profile` | `PATCH` | body no longer accepts `avatarUrl` (only `firstName` / `lastName`); avatar URL is set server-side by `spectrum/avatar/commit` |
| `api.projects.:id.lines` | `GET` | response is now `{ lines, pendingRegistrations }` instead of a bare `SpectrumLine[]` array |

## Summary

- **23** added
- **1** removed
- **0** changed
- **35** unchanged
- **2** changed
- **33** unchanged
5 changes: 3 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"prepublishOnly": "bun run typecheck && bun run build"
},
"devDependencies": {
"@photon-ai/dashboard-api": "1.2.0",
"@photon-ai/dashboard-api": "1.6.12",
"@types/bun": "latest",
"@types/node": "^25.6.2",
"@types/update-notifier": "^6.0.8",
Expand Down
55 changes: 0 additions & 55 deletions src/commands/spectrum/avatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { readFile, stat } from "node:fs/promises";
import { extname } from "node:path";
import { getApi } from "~/lib/api.ts";
import { resolveProject } from "~/lib/api-context.ts";
import { PRODUCTION_URL } from "~/lib/env.ts";
import { SessionExpiredError } from "~/lib/errors.ts";
import { c, die, formatApiError } from "~/lib/output.ts";

Expand All @@ -21,7 +20,6 @@ export function registerSpectrumAvatar(spectrum: Command): void {
avatar
.command("upload <file>")
.description("upload an image as the Spectrum avatar")
.option("--no-update-profile", "only upload, don't update the profile to use the new avatar")
.option("-p, --project <id>", "project id (overrides $PHOTON_PROJECT_ID)")
.option("--api-host <url>", "API host URL (defaults to PHOTON_API_HOST or built-in production)")
.option("-t, --token <token>", "API token (overrides stored creds)")
Expand Down Expand Up @@ -97,60 +95,7 @@ export function registerSpectrumAvatar(spectrum: Command): void {
}
const avatarUrl = commitResult.avatarUrl;

// 4) Optionally update the Spectrum profile to point at the new URL.
if (opts.updateProfile !== false) {
const patch = await api.api
.projects({ id: projectId })
.spectrum.profile.patch({ avatarUrl });
if (patch.status === 401) throw new SessionExpiredError(resolved.name);
if (patch.error) {
// Upload + commit succeeded; surface the patch failure but don't
// undo. Build the recovery command with the same --project /
// --api-host / --token context the user originally passed, and
// quote the URL so shell-significant chars don't break copy-paste.
const recovery = buildRecoveryCommand({
projectId,
apiHost: resolved.url,
token: opts.token,
avatarUrl,
});
die(`Uploaded, but failed to update profile: ${formatApiError(patch.error)}`, {
hint: `Update manually: ${recovery}`,
context: `Avatar URL: ${avatarUrl}`,
});
}
}

console.log(c.success(`Uploaded avatar from ${file}`));
console.log(c.dim(` URL: ${avatarUrl}`));
});
}

function buildRecoveryCommand(opts: {
projectId: string;
apiHost: string;
token?: string;
avatarUrl: string;
}): string {
const parts: string[] = ["photon spectrum profile update"];
parts.push(`--project ${shellQuote(opts.projectId)}`);
// Only include --api-host if it differs from the default production URL,
// so the recovery command stays minimal in the common case.
if (opts.apiHost !== PRODUCTION_URL) {
parts.push(`--api-host ${shellQuote(opts.apiHost)}`);
}
if (opts.token !== undefined) {
parts.push(`--token ${shellQuote(opts.token)}`);
}
parts.push(`--avatar-url ${shellQuote(opts.avatarUrl)}`);
return parts.join(" ");
}

/**
* Single-quote a value for safe shell copy-paste. Escapes embedded
* single quotes via the standard `'\''` trick. Avoids shell injection
* vectors in URLs that contain `&`, `?`, `;`, `|`, etc.
*/
function shellQuote(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`;
}
2 changes: 1 addition & 1 deletion src/commands/spectrum/lines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function registerSpectrumLines(spectrum: Command): void {
if (status === 401) throw new SessionExpiredError(resolved.name);
if (error) die(`Failed to list lines: ${formatApiError(error)}`);

const list = (data ?? []) as SpectrumLine[];
const list = (data?.lines ?? []) as SpectrumLine[];
if (opts.json) return printJson(list);
if (list.length === 0) {
console.log(c.dim("No lines yet."));
Expand Down
9 changes: 3 additions & 6 deletions src/commands/spectrum/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,17 @@ export function registerSpectrumProfile(spectrum: Command): void {
.description("update the Spectrum profile (preserves unset fields)")
.option("--first-name <name>")
.option("--last-name <name>")
.option("--avatar-url <url>", "avatar image URL (use `spectrum avatar upload` instead)")
.option("-p, --project <id>", "project id (overrides $PHOTON_PROJECT_ID)")
.option("--api-host <url>", "API host URL (defaults to PHOTON_API_HOST or built-in production)")
.option("-t, --token <token>", "API token (overrides stored creds)")
.option("--json", "output JSON")
.action(async (opts) => {
const hasMutation =
opts.firstName !== undefined ||
opts.lastName !== undefined ||
opts.avatarUrl !== undefined;
opts.lastName !== undefined;
if (!hasMutation) {
die("Nothing to update.", {
hint: "Pass at least one of --first-name / --last-name / --avatar-url.",
hint: "Pass at least one of --first-name / --last-name (avatar: use `spectrum avatar upload`).",
});
}

Expand All @@ -83,10 +81,9 @@ export function registerSpectrumProfile(spectrum: Command): void {
requireAuth: true,
});

const body: Record<string, string> = {};
const body: { firstName?: string; lastName?: string } = {};
if (opts.firstName !== undefined) body.firstName = opts.firstName;
if (opts.lastName !== undefined) body.lastName = opts.lastName;
if (opts.avatarUrl !== undefined) body.avatarUrl = opts.avatarUrl;

const { data, error, status } = await api.api
.projects({ id: projectId })
Expand Down
Loading