From 69fbc74fe231f078b339c76805a7bd8530e91a27 Mon Sep 17 00:00:00 2001 From: Personal Stack Agent Date: Sat, 6 Jun 2026 10:27:57 +0000 Subject: [PATCH 1/2] assistant-ui: surface GitHub App install + permission links per repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Managing the personal-stack-agents GitHub App — installing it on a repo's owner, and periodically widening the token's permissions and approving that on each installation — meant leaving the UI to hunt for GitHub URLs. The repositories feature now surfaces those links directly. A pure helper (features/repositories/services/githubAppLinks.ts) parses the owner from a repository URL (scp-style SSH, ssh://, http(s)://, rejecting non-GitHub and malformed values) and builds the install URL from a slug read from VITE_GITHUB_APP_SLUG (default personal-stack-agents). A per-repository GitHubAppPanel shows the detected owner with install, user- and org-installation configure, and owner-side permissions links; a repo-agnostic GitHubAppReference on the repositories overview carries the operator-side permissions and installations links plus the per-installation approval note. The assistant-ui Dockerfile gains a VITE_GITHUB_APP_SLUG build arg. Frontend-only by design: no assistant-api or OpenAPI change, so the links are built client-side and the committed contract is untouched. --- services/assistant-ui/Dockerfile | 2 + .../__tests__/GitHubAppReference.test.ts | 47 ++++++ .../__tests__/RepositoriesView.test.ts | 52 +++++++ .../__tests__/RepositoryView.test.ts | 26 ++++ .../__tests__/githubAppLinks.test.ts | 134 ++++++++++++++++++ .../components/GitHubAppPanel.vue | 109 ++++++++++++++ .../components/GitHubAppReference.vue | 91 ++++++++++++ .../repositories/services/githubAppLinks.ts | 117 +++++++++++++++ .../repositories/views/RepositoriesView.vue | 3 + .../repositories/views/RepositoryView.vue | 3 + 10 files changed, 584 insertions(+) create mode 100644 services/assistant-ui/src/features/repositories/__tests__/GitHubAppReference.test.ts create mode 100644 services/assistant-ui/src/features/repositories/__tests__/RepositoriesView.test.ts create mode 100644 services/assistant-ui/src/features/repositories/__tests__/githubAppLinks.test.ts create mode 100644 services/assistant-ui/src/features/repositories/components/GitHubAppPanel.vue create mode 100644 services/assistant-ui/src/features/repositories/components/GitHubAppReference.vue create mode 100644 services/assistant-ui/src/features/repositories/services/githubAppLinks.ts diff --git a/services/assistant-ui/Dockerfile b/services/assistant-ui/Dockerfile index efc06b36..3bf7acaa 100644 --- a/services/assistant-ui/Dockerfile +++ b/services/assistant-ui/Dockerfile @@ -13,8 +13,10 @@ COPY libs/vue-common/ libs/vue-common/ COPY services/assistant-ui/ services/assistant-ui/ ARG VITE_AUTH_URL=https://auth.jorisjonkers.dev ARG VITE_FARO_URL=https://faro.jorisjonkers.dev/collect +ARG VITE_GITHUB_APP_SLUG=personal-stack-agents RUN VITE_AUTH_URL=${VITE_AUTH_URL} \ VITE_FARO_URL=${VITE_FARO_URL} \ + VITE_GITHUB_APP_SLUG=${VITE_GITHUB_APP_SLUG} \ pnpm --filter @personal-stack/assistant-ui build FROM nginx:alpine diff --git a/services/assistant-ui/src/features/repositories/__tests__/GitHubAppReference.test.ts b/services/assistant-ui/src/features/repositories/__tests__/GitHubAppReference.test.ts new file mode 100644 index 00000000..88550e39 --- /dev/null +++ b/services/assistant-ui/src/features/repositories/__tests__/GitHubAppReference.test.ts @@ -0,0 +1,47 @@ +import type { DOMWrapper } from '@vue/test-utils' +import { mount } from '@vue/test-utils' +import { afterEach, describe, expect, it, vi } from 'vitest' +import GitHubAppReference from '../components/GitHubAppReference.vue' +import { GITHUB_APP_SLUG_ENV_KEY } from '../services/githubAppLinks' + +function expectSafeNewTab(link: Omit, 'exists'>, href: string): void { + expect(link.attributes('href')).toBe(href) + expect(link.attributes('target')).toBe('_blank') + expect(link.attributes('rel')).toBe('noopener noreferrer') +} + +describe('gitHubAppReference', () => { + afterEach(() => { + vi.unstubAllEnvs() + }) + + it('renders canonical GitHub App links with safe new-tab attributes', () => { + vi.stubEnv(GITHUB_APP_SLUG_ENV_KEY, 'custom-agents-app') + const wrapper = mount(GitHubAppReference) + + expectSafeNewTab(wrapper.get('[data-testid="github-app-public-link"]'), 'https://github.com/apps/custom-agents-app') + expectSafeNewTab( + wrapper.get('[data-testid="github-app-install-link"]'), + 'https://github.com/apps/custom-agents-app/installations/new', + ) + expectSafeNewTab( + wrapper.get('[data-testid="github-app-owner-permissions-link"]'), + 'https://github.com/settings/apps/custom-agents-app/permissions', + ) + expectSafeNewTab( + wrapper.get('[data-testid="github-app-owner-installations-link"]'), + 'https://github.com/settings/apps/custom-agents-app/installations', + ) + }) + + it('summarizes requested permissions and installation approval', () => { + const wrapper = mount(GitHubAppReference) + + expect(wrapper.get('[data-testid="github-app-permission-contents"]').text()).toBe('Contents: write') + expect(wrapper.get('[data-testid="github-app-permission-pull_requests"]').text()).toBe('Pull requests: write') + expect(wrapper.get('[data-testid="github-app-permission-actions"]').text()).toBe('Actions: write') + expect(wrapper.get('[data-testid="github-app-permission-approval-note"]').text()).toContain( + 'Permission changes must be approved on each existing installation', + ) + }) +}) diff --git a/services/assistant-ui/src/features/repositories/__tests__/RepositoriesView.test.ts b/services/assistant-ui/src/features/repositories/__tests__/RepositoriesView.test.ts new file mode 100644 index 00000000..72bd59b0 --- /dev/null +++ b/services/assistant-ui/src/features/repositories/__tests__/RepositoriesView.test.ts @@ -0,0 +1,52 @@ +import type { Repository } from '../types' +import { mount } from '@vue/test-utils' +import { createPinia, setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createMemoryHistory, createRouter } from 'vue-router' +import RepositoriesView from '../views/RepositoriesView.vue' + +const listRepositories = vi.fn<() => Promise>() + +vi.mock('../services/repositoriesService', () => ({ + listRepositories: () => listRepositories(), + getRepository: vi.fn(), + createRepository: vi.fn(), + attachDeployKey: vi.fn(), + deleteRepository: vi.fn(), + verifyRepositoryAccess: vi.fn(), +})) + +const router = createRouter({ + history: createMemoryHistory(), + routes: [ + { path: '/repositories', component: RepositoriesView }, + { path: '/repositories/:id', component: { template: '
' } }, + ], +}) + +describe('repositoriesView', () => { + beforeEach(async () => { + setActivePinia(createPinia()) + listRepositories.mockReset() + await router.push('/repositories') + await router.isReady() + }) + + it('places the GitHub App reference below the header and above loading state', async () => { + listRepositories.mockReturnValue(new Promise(() => {})) + + const wrapper = mount(RepositoriesView, { global: { plugins: [router] } }) + await wrapper.vm.$nextTick() + + const header = wrapper.get('header') + const reference = wrapper.get('[data-testid="github-app-reference"]') + const loading = wrapper.findAll('div').find((node) => node.text().startsWith('Loading')) + if (loading === undefined) throw new Error('missing loading state') + + expect(header.element.compareDocumentPosition(reference.element)).toBe(Node.DOCUMENT_POSITION_FOLLOWING) + expect(reference.element.compareDocumentPosition(loading.element)).toBe(Node.DOCUMENT_POSITION_FOLLOWING) + expect(reference.get('[data-testid="github-app-public-link"]').attributes('href')).toBe( + 'https://github.com/apps/personal-stack-agents', + ) + }) +}) diff --git a/services/assistant-ui/src/features/repositories/__tests__/RepositoryView.test.ts b/services/assistant-ui/src/features/repositories/__tests__/RepositoryView.test.ts index 860130a4..8cf26d60 100644 --- a/services/assistant-ui/src/features/repositories/__tests__/RepositoryView.test.ts +++ b/services/assistant-ui/src/features/repositories/__tests__/RepositoryView.test.ts @@ -106,6 +106,32 @@ describe('repositoryView access status', () => { expect(wrapper.find('[data-testid="repository-protection-unknown"]').exists()).toBe(true) }) + it('renders GitHub App owner guidance and canonical external links', async () => { + getRepository.mockResolvedValue(detail({ repository: fakeRepo({ repoUrl: 'git@github.com:ExtraToast/demo.git' }) })) + const wrapper = await mountView() + + expect(wrapper.find('[data-testid="github-app-owner"]').text()).toBe('ExtraToast') + expect(wrapper.find('[data-testid="github-app-permissions"]').text()).toMatch(/Contents:\s+write/) + expect(wrapper.find('[data-testid="github-app-permissions"]').text()).toMatch(/Pull requests:\s+write/) + expect(wrapper.find('[data-testid="github-app-permissions"]').text()).toMatch(/Actions:\s+write/) + expect(wrapper.find('[data-testid="github-app-approval-note"]').text()).toContain('approval on each installation') + + const expectedLinks = { + 'github-app-install-link': 'https://github.com/apps/personal-stack-agents/installations/new?state=ExtraToast', + 'github-app-user-installations-link': 'https://github.com/settings/installations', + 'github-app-organization-installations-link': + 'https://github.com/organizations/ExtraToast/settings/installations', + 'github-app-permissions-link': 'https://github.com/settings/apps/personal-stack-agents/permissions', + } + + for (const [testId, href] of Object.entries(expectedLinks)) { + const link = wrapper.find(`[data-testid="${testId}"]`) + expect(link.attributes('href')).toBe(href) + expect(link.attributes('target')).toBe('_blank') + expect(link.attributes('rel')).toBe('noopener noreferrer') + } + }) + it('verify-access button calls the verify endpoint and stores the result', async () => { getRepository.mockResolvedValue(detail()) verifyRepositoryAccess.mockResolvedValue({ diff --git a/services/assistant-ui/src/features/repositories/__tests__/githubAppLinks.test.ts b/services/assistant-ui/src/features/repositories/__tests__/githubAppLinks.test.ts new file mode 100644 index 00000000..1661c631 --- /dev/null +++ b/services/assistant-ui/src/features/repositories/__tests__/githubAppLinks.test.ts @@ -0,0 +1,134 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { + buildGitHubAppInstallationUrl, + buildGitHubAppInstallationUrlForRepo, + DEFAULT_GITHUB_APP_SLUG, + GITHUB_APP_REQUESTED_PERMISSIONS, + GITHUB_APP_SLUG_ENV_KEY, + githubAppLinks, + isValidGitHubAppSlug, + isValidGitHubOwner, + parseGitHubRepositoryOwner, + parseGitHubRepositoryUrl, + resolveGitHubAppSlug, +} from '../services/githubAppLinks' + +describe('githubAppLinks', () => { + afterEach(() => { + vi.unstubAllEnvs() + }) + + it.each([ + ['git@github.com:ExtraToast/personal-stack.git', 'ExtraToast', 'personal-stack'], + ['git@github.com:ExtraToast/personal-stack', 'ExtraToast', 'personal-stack'], + ['ssh://git@github.com/ExtraToast/personal-stack.git', 'ExtraToast', 'personal-stack'], + ['https://github.com/ExtraToast/personal-stack.git', 'ExtraToast', 'personal-stack'], + ['https://github.com/ExtraToast/personal-stack', 'ExtraToast', 'personal-stack'], + ['https://github.com/ExtraToast/personal-stack/', 'ExtraToast', 'personal-stack'], + ['https://x-access-token:tok@github.com/ExtraToast/repo.git', 'ExtraToast', 'repo'], + ])('parses owner and repo from %s', (repoUrl, owner, repo) => { + expect(parseGitHubRepositoryUrl(repoUrl)).toEqual({ owner, repo }) + expect(parseGitHubRepositoryOwner(repoUrl)).toBe(owner) + }) + + it.each([ + 'not-a-url', + 'git@github.com:onlyowner', + 'https://github.com/onlyowner', + 'https://github.com/owner/repo/issues', + '', + ])('returns null for invalid repo URL format %s', (repoUrl) => { + expect(parseGitHubRepositoryUrl(repoUrl)).toBeNull() + expect(parseGitHubRepositoryOwner(repoUrl)).toBeNull() + }) + + it.each([ + { owner: 'ExtraToast', expected: true }, + { owner: 'octo-org-1', expected: true }, + { owner: 'a', expected: true }, + { owner: '', expected: false }, + { owner: '-octo', expected: false }, + { owner: 'octo-', expected: false }, + { owner: 'octo--org', expected: false }, + { owner: 'octo/org', expected: false }, + { owner: 'octo org', expected: false }, + { owner: 'abcdefghijklmnopqrstuvwxyz0123456789abcd', expected: false }, + ])('validates owner $owner as $expected', ({ owner, expected }) => { + expect(isValidGitHubOwner(owner)).toBe(expected) + }) + + it('rejects repo URLs with invalid owners', () => { + expect(parseGitHubRepositoryUrl('git@github.com:-octo/repo.git')).toBeNull() + expect(parseGitHubRepositoryUrl('https://github.com/octo--org/repo')).toBeNull() + }) + + it.each([ + { slug: 'personal-stack-agents', expected: true }, + { slug: 'custom-app-1', expected: true }, + { slug: 'a', expected: true }, + { slug: '', expected: false }, + { slug: 'Custom-App', expected: false }, + { slug: 'custom_app', expected: false }, + { slug: '-custom-app', expected: false }, + { slug: 'custom-app-', expected: false }, + ])('validates app slug $slug as $expected', ({ slug, expected }) => { + expect(isValidGitHubAppSlug(slug)).toBe(expected) + }) + + it('falls back to the default slug when no env or override is configured', () => { + expect(resolveGitHubAppSlug()).toBe(DEFAULT_GITHUB_APP_SLUG) + }) + + it('uses the env slug when configured', () => { + vi.stubEnv(GITHUB_APP_SLUG_ENV_KEY, 'custom-agents-app') + + expect(resolveGitHubAppSlug()).toBe('custom-agents-app') + }) + + it('prefers an explicit slug override over the env slug', () => { + vi.stubEnv(GITHUB_APP_SLUG_ENV_KEY, 'env-agents-app') + + expect(resolveGitHubAppSlug('override-agents-app')).toBe('override-agents-app') + }) + + it('rejects an invalid configured slug', () => { + vi.stubEnv(GITHUB_APP_SLUG_ENV_KEY, 'Bad Slug') + + expect(() => resolveGitHubAppSlug()).toThrow('Invalid GitHub App slug: Bad Slug') + expect(() => resolveGitHubAppSlug('also_bad')).toThrow('Invalid GitHub App slug: also_bad') + }) + + it('builds the canonical GitHub App installation URL with owner state', () => { + expect(buildGitHubAppInstallationUrl('ExtraToast')).toBe( + 'https://github.com/apps/personal-stack-agents/installations/new?state=ExtraToast', + ) + expect(buildGitHubAppInstallationUrlForRepo('git@github.com:ExtraToast/personal-stack.git')).toBe( + 'https://github.com/apps/personal-stack-agents/installations/new?state=ExtraToast', + ) + }) + + it('safely encodes the app slug path segment', () => { + expect(buildGitHubAppInstallationUrl('ExtraToast', 'custom-app')).toBe( + 'https://github.com/apps/custom-app/installations/new?state=ExtraToast', + ) + expect(() => buildGitHubAppInstallationUrl('ExtraToast', 'custom/app')).toThrow( + 'Invalid GitHub App slug: custom/app', + ) + }) + + it('rejects invalid owners before building URLs', () => { + expect(() => buildGitHubAppInstallationUrl('octo/org')).toThrow('Invalid GitHub owner: octo/org') + expect(buildGitHubAppInstallationUrlForRepo('not-a-url')).toBeNull() + }) + + it('mirrors the backend requested permissions without administration', () => { + expect(GITHUB_APP_REQUESTED_PERMISSIONS).toEqual([ + { key: 'contents', access: 'write', label: 'Contents' }, + { key: 'pull_requests', access: 'write', label: 'Pull requests' }, + { key: 'actions', access: 'write', label: 'Actions' }, + ]) + expect(GITHUB_APP_REQUESTED_PERMISSIONS.map((permission) => permission.key)).not.toContain('administration') + expect(Object.isFrozen(GITHUB_APP_REQUESTED_PERMISSIONS)).toBe(true) + expect(Object.isFrozen(githubAppLinks)).toBe(true) + }) +}) diff --git a/services/assistant-ui/src/features/repositories/components/GitHubAppPanel.vue b/services/assistant-ui/src/features/repositories/components/GitHubAppPanel.vue new file mode 100644 index 00000000..1a8a877b --- /dev/null +++ b/services/assistant-ui/src/features/repositories/components/GitHubAppPanel.vue @@ -0,0 +1,109 @@ + + + diff --git a/services/assistant-ui/src/features/repositories/components/GitHubAppReference.vue b/services/assistant-ui/src/features/repositories/components/GitHubAppReference.vue new file mode 100644 index 00000000..14122737 --- /dev/null +++ b/services/assistant-ui/src/features/repositories/components/GitHubAppReference.vue @@ -0,0 +1,91 @@ + + + diff --git a/services/assistant-ui/src/features/repositories/services/githubAppLinks.ts b/services/assistant-ui/src/features/repositories/services/githubAppLinks.ts new file mode 100644 index 00000000..137b85b9 --- /dev/null +++ b/services/assistant-ui/src/features/repositories/services/githubAppLinks.ts @@ -0,0 +1,117 @@ +export interface GitHubRepositorySlug { + readonly owner: string + readonly repo: string +} + +export interface GitHubAppRequestedPermission { + readonly key: string + readonly access: string + readonly label: string +} + +export interface GitHubAppLinksApi { + readonly requestedPermissions: ReadonlyArray + readonly isValidGitHubAppSlug: (slug: string) => boolean + readonly resolveGitHubAppSlug: (override?: string | null) => string + readonly isValidGitHubOwner: (owner: string) => boolean + readonly parseGitHubRepositoryUrl: (repoUrl: string) => GitHubRepositorySlug | null + readonly parseGitHubRepositoryOwner: (repoUrl: string) => string | null + readonly buildGitHubAppInstallationUrl: (owner: string, appSlug?: string | null) => string + readonly buildGitHubAppInstallationUrlForRepo: (repoUrl: string, appSlug?: string | null) => string | null +} + +export const DEFAULT_GITHUB_APP_SLUG = 'personal-stack-agents' +export const GITHUB_APP_SLUG_ENV_KEY = 'VITE_GITHUB_APP_SLUG' + +const GITHUB_BASE_URL = 'https://github.com' +const GITHUB_APP_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,98}[a-z0-9])?$/ +const GITHUB_OWNER_PATTERN = /^(?!.*--)[A-Z0-9](?:[A-Z0-9-]{0,37}[A-Z0-9])?$/i +const SCP_LIKE_REPO_URL = /^[^@]+@[^:]+:([^/]+)\/([^/]+?)(?:\.git)?\/?$/ +const URL_LIKE_REPO_URL = /^[a-zA-Z]+:\/\/[^/]+\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/ + +function permission(key: string, access: string, label: string): GitHubAppRequestedPermission { + return Object.freeze({ key, access, label }) +} + +export const GITHUB_APP_REQUESTED_PERMISSIONS: ReadonlyArray = Object.freeze([ + permission('contents', 'write', 'Contents'), + permission('pull_requests', 'write', 'Pull requests'), + permission('actions', 'write', 'Actions'), +]) + +export function isValidGitHubAppSlug(slug: string): boolean { + return GITHUB_APP_SLUG_PATTERN.test(slug.trim()) +} + +export function resolveGitHubAppSlug(override?: string | null): string { + const candidate = firstConfiguredSlug(override, import.meta.env[GITHUB_APP_SLUG_ENV_KEY], DEFAULT_GITHUB_APP_SLUG) + if (!isValidGitHubAppSlug(candidate)) { + throw new Error(`Invalid GitHub App slug: ${candidate}`) + } + return candidate +} + +export function isValidGitHubOwner(owner: string): boolean { + return GITHUB_OWNER_PATTERN.test(owner.trim()) +} + +export function parseGitHubRepositoryUrl(repoUrl: string): GitHubRepositorySlug | null { + const trimmed = repoUrl.trim() + const match = SCP_LIKE_REPO_URL.exec(trimmed) ?? URL_LIKE_REPO_URL.exec(trimmed) + if (match === null) { + return null + } + const owner = match[1]?.trim() + const repo = match[2]?.trim() + if (owner === undefined || repo === undefined || owner === '' || repo === '' || !isValidGitHubOwner(owner)) { + return null + } + return Object.freeze({ owner, repo }) +} + +export function parseGitHubRepositoryOwner(repoUrl: string): string | null { + return parseGitHubRepositoryUrl(repoUrl)?.owner ?? null +} + +export function buildGitHubAppInstallationUrl(owner: string, appSlug?: string | null): string { + const trimmedOwner = owner.trim() + if (!isValidGitHubOwner(trimmedOwner)) { + throw new Error(`Invalid GitHub owner: ${owner}`) + } + const encodedSlug = encodeURIComponent(resolveGitHubAppSlug(appSlug)) + const url = new URL(`${GITHUB_BASE_URL}/apps/${encodedSlug}/installations/new`) + url.searchParams.set('state', trimmedOwner) + return url.toString() +} + +export function buildGitHubAppInstallationUrlForRepo(repoUrl: string, appSlug?: string | null): string | null { + const owner = parseGitHubRepositoryOwner(repoUrl) + if (owner === null) { + return null + } + return buildGitHubAppInstallationUrl(owner, appSlug) +} + +function firstConfiguredSlug( + override: string | null | undefined, + env: string | boolean | undefined, + fallback: string, +): string { + const overrideSlug = override?.trim() + if (overrideSlug !== undefined && overrideSlug !== '') { + return overrideSlug + } + const envSlug = typeof env === 'string' ? env.trim() : '' + return envSlug === '' ? fallback : envSlug +} + +export const githubAppLinks: GitHubAppLinksApi = Object.freeze({ + requestedPermissions: GITHUB_APP_REQUESTED_PERMISSIONS, + isValidGitHubAppSlug, + resolveGitHubAppSlug, + isValidGitHubOwner, + parseGitHubRepositoryUrl, + parseGitHubRepositoryOwner, + buildGitHubAppInstallationUrl, + buildGitHubAppInstallationUrlForRepo, +}) diff --git a/services/assistant-ui/src/features/repositories/views/RepositoriesView.vue b/services/assistant-ui/src/features/repositories/views/RepositoriesView.vue index 8575bc1d..3923e3d4 100644 --- a/services/assistant-ui/src/features/repositories/views/RepositoriesView.vue +++ b/services/assistant-ui/src/features/repositories/views/RepositoriesView.vue @@ -4,6 +4,7 @@ import { Card, Modal, useToast } from '@personal-stack/vue-common' import { onMounted, ref } from 'vue' import { useRouter } from 'vue-router' import CreateRepositoryForm from '../components/CreateRepositoryForm.vue' +import GitHubAppReference from '../components/GitHubAppReference.vue' import { useRepositoriesStore } from '../stores/repositories' const store = useRepositoriesStore() @@ -52,6 +53,8 @@ async function onCreateSuccess(created: Repository): Promise { + +

{{ store.error }}

Loading…
diff --git a/services/assistant-ui/src/features/repositories/views/RepositoryView.vue b/services/assistant-ui/src/features/repositories/views/RepositoryView.vue index f3efd861..d40417ec 100644 --- a/services/assistant-ui/src/features/repositories/views/RepositoryView.vue +++ b/services/assistant-ui/src/features/repositories/views/RepositoryView.vue @@ -5,6 +5,7 @@ import { computed, onMounted, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' import AccessStatusBadge from '../components/AccessStatusBadge.vue' import AttachKeyWizard from '../components/AttachKeyWizard.vue' +import GitHubAppPanel from '../components/GitHubAppPanel.vue' import { useRepositoriesStore } from '../stores/repositories' const route = useRoute() @@ -129,6 +130,8 @@ async function onDestroy(): Promise {
+ +

Access

From 1c73f1d8743b445d7501922b5bbb34b19f7da1ff Mon Sep 17 00:00:00 2001 From: Personal Stack Agent Date: Sat, 6 Jun 2026 10:39:35 +0000 Subject: [PATCH 2/2] chore: exclude agent-kit content + private docs from prettier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repo-wide `pnpm format:check` (`prettier --check .`) only runs in the "Lint Frontend" job, which is path-gated to frontend changes — so the agent-kit prompt/template/skill/manifest files and private design notes have been edited for a while without prettier ever checking them, leaving them in a non-prettier style. The first PR to touch a frontend path surfaces that as 21 "code style" failures unrelated to the change. These are authored/generated text — prompt wording, rendered skills, the manifest whose sha256s are pinned by the agent-kit renderer, and private design docs — not JS sources, and prettier reformatting them either churns prompt text or fights the renderer. Exclude them from prettier, matching how the ignore file already excludes generated artefacts (openapi.json, generated.ts, generated-jooq). --- .prettierignore | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.prettierignore b/.prettierignore index eb74930f..0f14cd10 100644 --- a/.prettierignore +++ b/.prettierignore @@ -14,3 +14,12 @@ playwright-report services/assistant-api/openapi.json **/src/api/generated.ts .gradle + +# Agent-kit content: prompts, templates, rendered skills, the manifest, and +# design notes are authored/generated text (and prompt wording), not JS sources. +# Keep prettier off them so it neither churns prompt text nor fights the +# agent-kit renderer / manifest sha pins. +.agents/skills/ +.claude/skills/ +platform/agents/ +docs/private/