sf snapshot: uncommitted changes after 93m inactivity
This commit is contained in:
parent
a73ea845e7
commit
f655188814
12 changed files with 302 additions and 19 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.4.13/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ Coverage thresholds (enforced by `npm run test:coverage`):
|
||||||
- Lines: **40%** minimum
|
- Lines: **40%** minimum
|
||||||
- Branches: **20%** minimum
|
- Branches: **20%** minimum
|
||||||
- Functions: **20%** minimum
|
- Functions: **20%** minimum
|
||||||
|
- Autonomous path overrides:
|
||||||
|
- `src/resources/extensions/sf/auto/**`: **60%** statements/lines/functions, **40%** branches
|
||||||
|
- `src/resources/extensions/sf/uok/**`: **60%** statements/lines/functions, **40%** branches
|
||||||
|
|
||||||
These are floors, not targets. The real quality bar is purposeful tests that assert behavior contracts (see `docs/SPEC_FIRST_TDD.md`).
|
These are floors, not targets. The real quality bar is purposeful tests that assert behavior contracts (see `docs/SPEC_FIRST_TDD.md`).
|
||||||
|
|
||||||
|
|
|
||||||
74
docs/dev/SETUP.md
Normal file
74
docs/dev/SETUP.md
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
# Developer Setup
|
||||||
|
|
||||||
|
This page is the short path for contributors who already have the repository
|
||||||
|
checked out and want a working local SF development environment.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js 24 or newer
|
||||||
|
- npm
|
||||||
|
- Git
|
||||||
|
- Rust toolchain for native engine work
|
||||||
|
- GitHub CLI for label, issue, and PR workflows
|
||||||
|
- Docker or a compatible container runtime for devcontainer verification
|
||||||
|
|
||||||
|
## First-Time Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run secret-scan:install-hook
|
||||||
|
node scripts/tech-debt-scan.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional but recommended:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
devcontainer build --workspace-folder .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Daily Checks
|
||||||
|
|
||||||
|
Use the narrowest command that proves the change:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run lint
|
||||||
|
npm run typecheck:extensions
|
||||||
|
npm run test:unit
|
||||||
|
```
|
||||||
|
|
||||||
|
Before shipping changes that affect the CLI runtime:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:core
|
||||||
|
npm run test:smoke
|
||||||
|
```
|
||||||
|
|
||||||
|
For coverage-sensitive changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
The global floor is intentionally modest, but autonomous paths have stricter
|
||||||
|
coverage thresholds in `vitest.config.ts`.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
SF-specific environment variables are parsed through `src/env.ts` for typed
|
||||||
|
callers. Add new `SF_*` variables there before introducing new production reads.
|
||||||
|
Document user-facing variables in `docs/user-docs/configuration.md`.
|
||||||
|
|
||||||
|
Common local overrides:
|
||||||
|
|
||||||
|
| Variable | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| `SF_HOME` | Redirect global SF runtime state away from `~/.sf`. |
|
||||||
|
| `SF_AGENT_DIR` | Override the managed agent directory used by headless/runtime loaders. |
|
||||||
|
| `SF_PROJECT_ID` | Override the project state key; must be alphanumeric, hyphen, or underscore. |
|
||||||
|
| `SF_BIN_PATH` | Point child processes at a specific SF loader. |
|
||||||
|
|
||||||
|
## Contribution Contract
|
||||||
|
|
||||||
|
Every behavioral change starts with a failing behavior test or executable
|
||||||
|
evidence. Keep commits focused, avoid drive-by formatting, and do not commit
|
||||||
|
transient `.sf/` runtime state.
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
# File Reference — Example Extensions
|
# File Reference — Example Extensions
|
||||||
|
|
||||||
|
|
||||||
All paths relative to:
|
Upstream example extensions are copied into the managed agent directory during
|
||||||
|
runtime setup. In source checkouts, use the bundled extension tree under
|
||||||
|
`src/resources/extensions/` for SF-owned examples and tests.
|
||||||
|
|
||||||
|
Common reference roots:
|
||||||
|
|
||||||
```
|
```
|
||||||
/Users/lexchristopherson/.nvm/versions/node/v22.20.0/lib/node_modules/@mariozechner/pi-coding-agent/examples/extensions/
|
src/resources/extensions/
|
||||||
|
~/.sf/agent/extensions/
|
||||||
```
|
```
|
||||||
|
|
||||||
### Lifecycle & Safety
|
### Lifecycle & Safety
|
||||||
|
|
@ -125,8 +131,6 @@ All paths relative to:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*This document was generated from the Pi extension documentation and examples. Source docs are at:*
|
*This document was generated from the Pi extension documentation and examples.
|
||||||
```
|
Use repository-relative paths for SF source work and `~/.sf/agent/extensions/`
|
||||||
/Users/lexchristopherson/.nvm/versions/node/v22.20.0/lib/node_modules/@mariozechner/pi-coding-agent/docs/
|
for installed runtime inspection.*
|
||||||
/Users/lexchristopherson/.nvm/versions/node/v22.20.0/lib/node_modules/@mariozechner/pi-coding-agent/examples/extensions/
|
|
||||||
```
|
|
||||||
|
|
|
||||||
88
docs/dev/extending-pi/26-extension-template.md
Normal file
88
docs/dev/extending-pi/26-extension-template.md
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
# Extension Development Template
|
||||||
|
|
||||||
|
Use this template for small repo-local extensions before adding packaging,
|
||||||
|
dependencies, or distribution metadata.
|
||||||
|
|
||||||
|
## Directory Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.sf/agent/extensions/my-extension/
|
||||||
|
├── index.ts
|
||||||
|
├── extension-manifest.json
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
For source-tree development inside this repository, place bundled extensions
|
||||||
|
under `src/resources/extensions/<extension-id>/` and keep tests next to the
|
||||||
|
extension's existing test conventions.
|
||||||
|
|
||||||
|
## `extension-manifest.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "my-extension",
|
||||||
|
"name": "My Extension",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Adds one focused command or tool.",
|
||||||
|
"tier": "project",
|
||||||
|
"entry": "index.ts",
|
||||||
|
"provides": {
|
||||||
|
"tools": ["my_tool"],
|
||||||
|
"commands": ["/my-command"],
|
||||||
|
"hooks": []
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"extensions": [],
|
||||||
|
"runtime": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `index.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Type } from "@sinclair/typebox";
|
||||||
|
import type {
|
||||||
|
ExtensionAPI,
|
||||||
|
ExtensionContext,
|
||||||
|
} from "@mariozechner/pi-coding-agent";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the extension's command and tool.
|
||||||
|
*
|
||||||
|
* Purpose: expose one project-specific operation through a typed tool and a
|
||||||
|
* human-triggered command.
|
||||||
|
*
|
||||||
|
* Consumer: SF runtime extension loader.
|
||||||
|
*/
|
||||||
|
export default function activate(pi: ExtensionAPI, ctx: ExtensionContext) {
|
||||||
|
pi.registerTool({
|
||||||
|
name: "my_tool",
|
||||||
|
description: "Return a concise project-specific status.",
|
||||||
|
parameters: Type.Object({
|
||||||
|
label: Type.String({ description: "Status label to display." }),
|
||||||
|
}),
|
||||||
|
async execute({ label }) {
|
||||||
|
return { ok: true, label };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.registerCommand("my-command", {
|
||||||
|
description: "Show the current extension status.",
|
||||||
|
async run() {
|
||||||
|
ctx.ui.notify("My extension is loaded.", "info");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
|
||||||
|
- The extension has one clear purpose and a named production consumer.
|
||||||
|
- Every tool parameter has a TypeBox schema.
|
||||||
|
- Persistent state is written under the project `.sf/` tree or the managed
|
||||||
|
agent directory, not arbitrary home-directory paths.
|
||||||
|
- Commands and tools degrade with useful messages when dependencies are missing.
|
||||||
|
- Tests cover the behavior contract, not only mocks or call counts.
|
||||||
|
- User-facing environment variables are added to `src/env.ts` and
|
||||||
|
`docs/user-docs/configuration.md`.
|
||||||
|
|
@ -29,8 +29,8 @@
|
||||||
- [23. File Reference — Documentation](./23-file-reference-documentation.md)
|
- [23. File Reference — Documentation](./23-file-reference-documentation.md)
|
||||||
- [24. File Reference — Example Extensions](./24-file-reference-example-extensions.md)
|
- [24. File Reference — Example Extensions](./24-file-reference-example-extensions.md)
|
||||||
- [25. Slash Command Subcommand Patterns](./25-slash-command-subcommand-patterns.md)
|
- [25. Slash Command Subcommand Patterns](./25-slash-command-subcommand-patterns.md)
|
||||||
|
- [26. Extension Development Template](./26-extension-template.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Split into per-section files for surgical context loading.*
|
*Split into per-section files for surgical context loading.*
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { homedir } from "node:os";
|
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
import { getSfEnv } from "./env.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* app-paths.ts — central directory and file path constants for the sf runtime.
|
* app-paths.ts — central directory and file path constants for the sf runtime.
|
||||||
|
|
@ -26,7 +26,7 @@ import { join } from "node:path";
|
||||||
* remote-questions-config.ts (global preferences), extension-registry.ts
|
* remote-questions-config.ts (global preferences), extension-registry.ts
|
||||||
* (registry JSON), and every other derived path in this module.
|
* (registry JSON), and every other derived path in this module.
|
||||||
*/
|
*/
|
||||||
export const appRoot = process.env.SF_HOME || join(homedir(), ".sf");
|
export const appRoot = getSfEnv().sfHome;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the path to the managed agent directory.
|
* Returns the path to the managed agent directory.
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import {
|
||||||
unwatchFile,
|
unwatchFile,
|
||||||
watchFile,
|
watchFile,
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { homedir } from "node:os";
|
|
||||||
import { basename, join } from "node:path";
|
import { basename, join } from "node:path";
|
||||||
|
import { getSfEnv } from "./env.js";
|
||||||
|
|
||||||
export type LogSourceName = "notif" | "session" | "activity" | "audit";
|
export type LogSourceName = "notif" | "session" | "activity" | "audit";
|
||||||
|
|
||||||
|
|
@ -62,7 +62,7 @@ function normalizeSource(value: string | undefined): LogSourceName | undefined {
|
||||||
}
|
}
|
||||||
|
|
||||||
function sfHomeFromEnv(): string {
|
function sfHomeFromEnv(): string {
|
||||||
return process.env.SF_HOME || join(homedir(), ".sf");
|
return getSfEnv().sfHome;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProjectSessionKey(basePath: string): string {
|
export function getProjectSessionKey(basePath: string): string {
|
||||||
|
|
|
||||||
65
src/env.ts
Normal file
65
src/env.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const optionalNonEmptyString = z.string().trim().min(1).optional();
|
||||||
|
|
||||||
|
const booleanOneZero = z
|
||||||
|
.enum(["0", "1"])
|
||||||
|
.optional()
|
||||||
|
.transform((value) => value === "1");
|
||||||
|
|
||||||
|
export const sfEnvSchema = z.object({
|
||||||
|
SF_HOME: optionalNonEmptyString,
|
||||||
|
SF_AGENT_DIR: optionalNonEmptyString,
|
||||||
|
SF_CODING_AGENT_DIR: optionalNonEmptyString,
|
||||||
|
SF_STATE_DIR: optionalNonEmptyString,
|
||||||
|
SF_PROJECT_ID: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.regex(/^[A-Za-z0-9_-]+$/, {
|
||||||
|
message:
|
||||||
|
"SF_PROJECT_ID must contain only letters, numbers, hyphens, and underscores",
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
SF_BIN_PATH: optionalNonEmptyString,
|
||||||
|
SF_VERSION: optionalNonEmptyString,
|
||||||
|
SF_WEB_PROJECT_CWD: optionalNonEmptyString,
|
||||||
|
SF_WEB_DAEMON_MODE: booleanOneZero,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SfEnv = z.infer<typeof sfEnvSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse supported SF_* environment variables into a typed object.
|
||||||
|
*
|
||||||
|
* Purpose: give runtime code a shared contract for SF-specific environment
|
||||||
|
* variables instead of scattering ad hoc `process.env` parsing across entry
|
||||||
|
* points.
|
||||||
|
*
|
||||||
|
* Consumer: root CLI/headless modules and web bridge code that need stable SF
|
||||||
|
* path and mode values.
|
||||||
|
*/
|
||||||
|
export function parseSfEnv(env: NodeJS.ProcessEnv = process.env): SfEnv {
|
||||||
|
return sfEnvSchema.parse(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return typed SF environment values with path defaults applied.
|
||||||
|
*
|
||||||
|
* Purpose: centralize default path behavior for SF_HOME and the managed agent
|
||||||
|
* directory while still validating user-provided overrides.
|
||||||
|
*
|
||||||
|
* Consumer: app-paths.ts, cli-logs.ts, headless-query.ts, and future env readers.
|
||||||
|
*/
|
||||||
|
export function getSfEnv(env: NodeJS.ProcessEnv = process.env) {
|
||||||
|
const parsed = parseSfEnv(env);
|
||||||
|
const sfHome = parsed.SF_HOME ?? join(homedir(), ".sf");
|
||||||
|
const agentDir =
|
||||||
|
parsed.SF_AGENT_DIR ?? parsed.SF_CODING_AGENT_DIR ?? join(sfHome, "agent");
|
||||||
|
return {
|
||||||
|
...parsed,
|
||||||
|
sfHome,
|
||||||
|
agentDir,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -16,10 +16,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { existsSync, readFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
import { homedir } from "node:os";
|
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { createJiti } from "@mariozechner/jiti";
|
import { createJiti } from "@mariozechner/jiti";
|
||||||
import { resolveBundledSourceResource } from "./bundled-resource-path.js";
|
import { resolveBundledSourceResource } from "./bundled-resource-path.js";
|
||||||
|
import { getSfEnv } from "./env.js";
|
||||||
import type { SFState } from "./resources/extensions/sf/types.js";
|
import type { SFState } from "./resources/extensions/sf/types.js";
|
||||||
|
|
||||||
const jiti = createJiti(import.meta.filename, {
|
const jiti = createJiti(import.meta.filename, {
|
||||||
|
|
@ -29,11 +29,7 @@ const jiti = createJiti(import.meta.filename, {
|
||||||
// Resolve extensions from the synced agent directory so headless-query
|
// Resolve extensions from the synced agent directory so headless-query
|
||||||
// loads the same extension copy as interactive/auto modes (#3471).
|
// loads the same extension copy as interactive/auto modes (#3471).
|
||||||
// The synced runtime is compiled .js; source-tree fallback is .ts.
|
// The synced runtime is compiled .js; source-tree fallback is .ts.
|
||||||
const agentExtensionsDir = join(
|
const agentExtensionsDir = join(getSfEnv().agentDir, "extensions", "sf");
|
||||||
process.env.SF_AGENT_DIR || join(homedir(), ".sf", "agent"),
|
|
||||||
"extensions",
|
|
||||||
"sf",
|
|
||||||
);
|
|
||||||
const useAgentDir = existsSync(join(agentExtensionsDir, "state.js"));
|
const useAgentDir = existsSync(join(agentExtensionsDir, "state.js"));
|
||||||
const sfExtensionPath = (moduleName: string) => {
|
const sfExtensionPath = (moduleName: string) => {
|
||||||
if (useAgentDir) return join(agentExtensionsDir, `${moduleName}.js`);
|
if (useAgentDir) return join(agentExtensionsDir, `${moduleName}.js`);
|
||||||
|
|
|
||||||
41
src/tests/env.test.ts
Normal file
41
src/tests/env.test.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, test } from "vitest";
|
||||||
|
import { getSfEnv, parseSfEnv } from "../env.js";
|
||||||
|
|
||||||
|
describe("sf env schema", () => {
|
||||||
|
test("parseSfEnv_when_project_id_is_valid_returns_typed_values", () => {
|
||||||
|
const env = parseSfEnv({
|
||||||
|
SF_HOME: "/tmp/sf-home",
|
||||||
|
SF_PROJECT_ID: "repo_123-main",
|
||||||
|
SF_WEB_DAEMON_MODE: "1",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(env.SF_HOME, "/tmp/sf-home");
|
||||||
|
assert.equal(env.SF_PROJECT_ID, "repo_123-main");
|
||||||
|
assert.equal(env.SF_WEB_DAEMON_MODE, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseSfEnv_when_project_id_contains_path_separator_rejects", () => {
|
||||||
|
assert.throws(
|
||||||
|
() => parseSfEnv({ SF_PROJECT_ID: "../escape" }),
|
||||||
|
/SF_PROJECT_ID/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getSfEnv_when_agent_dir_unset_uses_sf_home_agent", () => {
|
||||||
|
const env = getSfEnv({ SF_HOME: "/tmp/sf-home" });
|
||||||
|
|
||||||
|
assert.equal(env.sfHome, "/tmp/sf-home");
|
||||||
|
assert.equal(env.agentDir, "/tmp/sf-home/agent");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getSfEnv_when_agent_dir_set_prefers_explicit_agent_dir", () => {
|
||||||
|
const env = getSfEnv({
|
||||||
|
SF_HOME: "/tmp/sf-home",
|
||||||
|
SF_AGENT_DIR: "/tmp/agent-dir",
|
||||||
|
SF_CODING_AGENT_DIR: "/tmp/coding-agent-dir",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(env.agentDir, "/tmp/agent-dir");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -225,6 +225,18 @@ export default defineConfig({
|
||||||
lines: 40,
|
lines: 40,
|
||||||
branches: 20,
|
branches: 20,
|
||||||
functions: 20,
|
functions: 20,
|
||||||
|
"src/resources/extensions/sf/auto/**": {
|
||||||
|
statements: 60,
|
||||||
|
lines: 60,
|
||||||
|
branches: 40,
|
||||||
|
functions: 60,
|
||||||
|
},
|
||||||
|
"src/resources/extensions/sf/uok/**": {
|
||||||
|
statements: 60,
|
||||||
|
lines: 60,
|
||||||
|
branches: 40,
|
||||||
|
functions: 60,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue