mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 18:44:45 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c2d11470f | |||
| dcd2f8f04b | |||
| ef3ddcdd7a | |||
| 5e207f760d | |||
| d616935296 | |||
| ea4032766b | |||
| 3e0d2db4c7 |
@@ -0,0 +1,38 @@
|
|||||||
|
## What's New in 2.2.1
|
||||||
|
|
||||||
|
A patch release covering Template Configuration rendering fixes reported against v2.2.0, plus a new catalog template that packages a Hermes skill for scaffolding new Scarf projects.
|
||||||
|
|
||||||
|
### Configuration sheet — no more clipping
|
||||||
|
|
||||||
|
Two independent rendering fixes to the post-install Configuration editor and the install-flow Configure step:
|
||||||
|
|
||||||
|
- **Enum fields with long option labels.** An enum with three or four options whose labels exceeded ~20 characters — e.g. a Claude-model picker with labels like *"Claude Opus 4 (Recommended - Most Capable)"* — rendered as a segmented picker that sized to the intrinsic width of all labels concatenated. On macOS, `.pickerStyle(.segmented)` refuses to respect offered width, refuses to wrap, refuses to truncate. The result was a ~650pt picker that overflowed the sheet's 560pt viewport and clipped the entire form on both sides. Enum fields now always render as a dropdown Menu picker, which surfaces long labels in the popup list and respects the parent's offered width regardless of option count or label length.
|
||||||
|
- **Descriptions with unbreakable content.** Field descriptions rendered via inline AttributedString markdown can contain tokens SwiftUI's `Text` refuses to break mid-token (raw URLs, long paths). Added `.frame(maxWidth: .infinity, alignment: .leading)` on the sheet's inner VStack and on each field row as a secondary constraint, so description text wraps at whitespace boundaries instead of expanding the sheet width. Applied the same modifier to `TemplateInstallSheet`'s main preview VStack for symmetry — installs with README blocks or cron prompts containing long URLs now wrap cleanly too.
|
||||||
|
|
||||||
|
### New catalog entry — `awizemann/template-author`
|
||||||
|
|
||||||
|
A `.scarftemplate` whose only content is a Hermes skill (`scarf-template-author`) plus a minimal dashboard that points users at it. Installing the template drops the skill at `~/.hermes/skills/templates/awizemann-template-author/scarf-template-author/SKILL.md`, discoverable by Claude Code, Cursor, Codex, Aider, and every other agent that reads the standard `~/.hermes/skills/` directory.
|
||||||
|
|
||||||
|
The skill teaches agents how to scaffold a new Scarf-compatible project through a short interview — purpose, data source, cadence, widgets, config, secrets — then write `<project>/.scarf/dashboard.json`, `<project>/.scarf/manifest.json`, `<project>/AGENTS.md`, and `<project>/README.md`. Scaffolded projects are usable locally and cleanly exportable as `.scarftemplate` bundles via Scarf's Export flow later. [Catalog detail page →](https://awizemann.github.io/scarf/templates/awizemann-template-author/)
|
||||||
|
|
||||||
|
v1 is fully conversational / blank-slate. Pre-baked archetypes (monitor, dev-dashboard, personal-log) are deferred to a future release pending real usage data.
|
||||||
|
|
||||||
|
### Authoring guidance — SKILL.md
|
||||||
|
|
||||||
|
The `scarf-template-author` skill now tells scaffolding agents to prefer markdown link syntax (`[label](https://…)`) over raw URLs in schema field descriptions. Raw URLs work now (v2.2.1's description wrap fix above handles them gracefully), but `[Anthropic console](https://console.anthropic.com)` reads cleaner in the form than a dumped URL. Same rule extended to long paths or other unbreakable strings — wrap in inline code if they have to appear verbatim, prefer markdown links otherwise.
|
||||||
|
|
||||||
|
### Under the hood
|
||||||
|
|
||||||
|
- **`scripts/catalog.sh publish` fix.** The pre-flight `need_ghpages` check tested `[[ -d "$GHPAGES_DIR/.git" ]]` — "is `.git` a directory?" — which is true for a regular clone but false for a `git worktree add` worktree (where `.git` is a pointer file). `release.sh` creates and leaves the gh-pages worktree around, so after any release the subsequent catalog-publish call was rejected with a misleading "run `git worktree add`" error on a worktree that was already there and valid. Switched to `-e` (exists, either file or directory). Unblocks publishing the catalog immediately after a release.
|
||||||
|
|
||||||
|
### Migrating from 2.2.0
|
||||||
|
|
||||||
|
Sparkle will offer the update automatically. No config migration needed. Existing template installs are untouched.
|
||||||
|
|
||||||
|
If you've already installed `awizemann/template-author` from a pre-release build, no action needed — the catalog and bundle content are forward-compatible.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- [Project Templates wiki page](https://github.com/awizemann/scarf/wiki/Project-Templates) — installing, exporting, configuring, authoring, uninstalling.
|
||||||
|
- [Catalog site](https://awizemann.github.io/scarf/templates/) — two templates live: `awizemann/site-status-checker` and `awizemann/template-author`.
|
||||||
|
- [`templates/CONTRIBUTING.md`](https://github.com/awizemann/scarf/blob/main/templates/CONTRIBUTING.md) — how to submit a template via PR.
|
||||||
@@ -436,7 +436,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements;
|
CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 23;
|
CURRENT_PROJECT_VERSION = 24;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
@@ -449,7 +449,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||||
MARKETING_VERSION = 2.2.0;
|
MARKETING_VERSION = 2.2.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarf.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarf.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
@@ -471,7 +471,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements;
|
CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 23;
|
CURRENT_PROJECT_VERSION = 24;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
@@ -484,7 +484,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||||
MARKETING_VERSION = 2.2.0;
|
MARKETING_VERSION = 2.2.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarf.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarf.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
@@ -502,12 +502,12 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 23;
|
CURRENT_PROJECT_VERSION = 24;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
||||||
MARKETING_VERSION = 2.2.0;
|
MARKETING_VERSION = 2.2.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarfTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarfTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||||
@@ -524,12 +524,12 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 23;
|
CURRENT_PROJECT_VERSION = 24;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
||||||
MARKETING_VERSION = 2.2.0;
|
MARKETING_VERSION = 2.2.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarfTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarfTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||||
@@ -545,11 +545,11 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 23;
|
CURRENT_PROJECT_VERSION = 24;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 2.2.0;
|
MARKETING_VERSION = 2.2.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarfUITests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarfUITests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||||
@@ -565,11 +565,11 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 23;
|
CURRENT_PROJECT_VERSION = 24;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 2.2.0;
|
MARKETING_VERSION = 2.2.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarfUITests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarfUITests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||||
|
|||||||
@@ -23,6 +23,20 @@ struct TemplateConfigSheet: View {
|
|||||||
header
|
header
|
||||||
Divider()
|
Divider()
|
||||||
ScrollView {
|
ScrollView {
|
||||||
|
// `.frame(maxWidth: .infinity, alignment: .leading)` is
|
||||||
|
// load-bearing: without it, SwiftUI resolves width
|
||||||
|
// bottom-up and an unbreakable token in a child (e.g. a
|
||||||
|
// raw URL inside a field description rendered via
|
||||||
|
// AttributedString markdown) sets the whole VStack's
|
||||||
|
// ideal width to that token's length. ScrollView's
|
||||||
|
// content then exceeds the sheet's viewport, the outer
|
||||||
|
// `.frame(minWidth: 560)` grows to content width, and
|
||||||
|
// the window clips the result with labels cut off on
|
||||||
|
// the left + URL spilling off the right. With the
|
||||||
|
// explicit maxWidth, the ScrollView's offered width
|
||||||
|
// propagates down and the description Text's
|
||||||
|
// `.fixedSize(horizontal: false, vertical: true)`
|
||||||
|
// wraps at whitespace boundaries as intended.
|
||||||
VStack(alignment: .leading, spacing: 18) {
|
VStack(alignment: .leading, spacing: 18) {
|
||||||
if viewModel.schema.fields.isEmpty {
|
if viewModel.schema.fields.isEmpty {
|
||||||
ContentUnavailableView(
|
ContentUnavailableView(
|
||||||
@@ -40,6 +54,7 @@ struct TemplateConfigSheet: View {
|
|||||||
modelRecommendation(rec)
|
modelRecommendation(rec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(20)
|
.padding(20)
|
||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
@@ -116,7 +131,11 @@ struct TemplateConfigSheet: View {
|
|||||||
// Inline markdown so descriptions can include
|
// Inline markdown so descriptions can include
|
||||||
// `[Create one](https://…)`-style links to token
|
// `[Create one](https://…)`-style links to token
|
||||||
// generation pages, **bold** emphasis on important
|
// generation pages, **bold** emphasis on important
|
||||||
// prerequisites, etc.
|
// prerequisites, etc. Raw URLs (not wrapped in
|
||||||
|
// markdown link syntax) will still render but can't
|
||||||
|
// word-break mid-token — keep the parent maxWidth
|
||||||
|
// constraint below so a rogue raw URL wraps cleanly
|
||||||
|
// instead of expanding the entire sheet.
|
||||||
TemplateMarkdown.inlineText(description)
|
TemplateMarkdown.inlineText(description)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
@@ -129,6 +148,12 @@ struct TemplateConfigSheet: View {
|
|||||||
.foregroundStyle(.red)
|
.foregroundStyle(.red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// maxWidth: .infinity forces this row to span the column's
|
||||||
|
// full width so its internal description Text wraps instead
|
||||||
|
// of expanding the outer VStack when a description contains
|
||||||
|
// a long unbreakable token (raw URL, path, etc.). See the
|
||||||
|
// comment on the parent ScrollView's inner VStack.
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(12)
|
.padding(12)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 8)
|
RoundedRectangle(cornerRadius: 8)
|
||||||
@@ -287,24 +312,23 @@ private struct EnumControl: View {
|
|||||||
let options: [TemplateConfigField.EnumOption]
|
let options: [TemplateConfigField.EnumOption]
|
||||||
@Binding var value: String
|
@Binding var value: String
|
||||||
var body: some View {
|
var body: some View {
|
||||||
// Segmented for ≤ 4 options, dropdown otherwise — fits Scarf's
|
// Always use the default Menu picker (dropdown). An earlier
|
||||||
// existing settings UI.
|
// version switched to `.pickerStyle(.segmented)` when
|
||||||
if options.count <= 4 {
|
// `options.count ≤ 4` for a more compact look, but on macOS
|
||||||
Picker("", selection: $value) {
|
// segmented pickers size to the intrinsic width of all their
|
||||||
ForEach(options) { opt in
|
// labels concatenated — they refuse offered width constraints
|
||||||
Text(opt.label).tag(opt.value)
|
// and refuse to wrap. A schema with three long labels like
|
||||||
}
|
// "Claude Opus 4 (Recommended - Most Capable)" produced a
|
||||||
|
// ~650pt picker that overflowed the 560pt sheet viewport,
|
||||||
|
// clipping the entire form. Menu pickers respect the fieldRow's
|
||||||
|
// offered width and show long labels in the popup list, so the
|
||||||
|
// sheet can't overflow regardless of label length.
|
||||||
|
Picker("", selection: $value) {
|
||||||
|
ForEach(options) { opt in
|
||||||
|
Text(opt.label).tag(opt.value)
|
||||||
}
|
}
|
||||||
.pickerStyle(.segmented)
|
|
||||||
.labelsHidden()
|
|
||||||
} else {
|
|
||||||
Picker("", selection: $value) {
|
|
||||||
ForEach(options) { opt in
|
|
||||||
Text(opt.label).tag(opt.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.labelsHidden()
|
|
||||||
}
|
}
|
||||||
|
.labelsHidden()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,16 @@ struct TemplateInstallSheet: View {
|
|||||||
.padding(.bottom, 8)
|
.padding(.bottom, 8)
|
||||||
Divider()
|
Divider()
|
||||||
ScrollView {
|
ScrollView {
|
||||||
|
// `.frame(maxWidth: .infinity, alignment: .leading)` —
|
||||||
|
// without it, a subsection containing an unbreakable
|
||||||
|
// token (raw URL in a cron prompt or README block, a
|
||||||
|
// long file path in the project-files list, a schema
|
||||||
|
// description with a bare URL, etc.) sets the VStack's
|
||||||
|
// ideal width to that token's length; the sheet grows
|
||||||
|
// past its `.frame(minWidth: 620)` and gets clipped by
|
||||||
|
// the window. Same fix as `TemplateConfigSheet`'s
|
||||||
|
// inner VStack — propagate the ScrollView's width down
|
||||||
|
// so inner Text wraps instead of expanding outward.
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
projectFilesSection(plan: plan)
|
projectFilesSection(plan: plan)
|
||||||
if plan.skillsNamespaceDir != nil {
|
if plan.skillsNamespaceDir != nil {
|
||||||
@@ -142,6 +152,7 @@ struct TemplateInstallSheet: View {
|
|||||||
}
|
}
|
||||||
readmeSection
|
readmeSection
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(.vertical)
|
.padding(.vertical)
|
||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
|
|||||||
@@ -1063,6 +1063,68 @@ final class TestRegistryLock: @unchecked Sendable {
|
|||||||
#expect(cronPrompt.contains("{{PROJECT_DIR}}"))
|
#expect(cronPrompt.contains("{{PROJECT_DIR}}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Exercises the second shipped template — `awizemann/template-author` —
|
||||||
|
/// which is a skill-only bundle (no config, no cron, no memory). The
|
||||||
|
/// shape is deliberately different from site-status-checker so a
|
||||||
|
/// regression in the installer's "no config, no cron" path can't hide
|
||||||
|
/// behind the richer example template. Also asserts the skill lands
|
||||||
|
/// under the expected namespaced path so Hermes's recursive skill
|
||||||
|
/// discovery finds it.
|
||||||
|
@Test func templateAuthorParsesAndPlans() throws {
|
||||||
|
let bundle = try Self.locateExample(author: "awizemann", name: "template-author")
|
||||||
|
|
||||||
|
let service = ProjectTemplateService(context: .local)
|
||||||
|
let inspection = try service.inspect(zipPath: bundle)
|
||||||
|
defer { service.cleanupTempDir(inspection.unpackedDir) }
|
||||||
|
|
||||||
|
// Manifest shape: schemaVersion 2 (contains `skills` claim, which
|
||||||
|
// wasn't part of v1), no config, no cron, one skill.
|
||||||
|
#expect(inspection.manifest.id == "awizemann/template-author")
|
||||||
|
#expect(inspection.manifest.name == "Scarf Template Author")
|
||||||
|
#expect(inspection.manifest.version == "1.0.0")
|
||||||
|
#expect(inspection.manifest.schemaVersion == 2)
|
||||||
|
#expect(inspection.manifest.contents.dashboard)
|
||||||
|
#expect(inspection.manifest.contents.agentsMd)
|
||||||
|
#expect(inspection.manifest.contents.cron == nil)
|
||||||
|
#expect(inspection.manifest.contents.config == nil)
|
||||||
|
#expect(inspection.manifest.contents.memory == nil)
|
||||||
|
#expect(inspection.manifest.contents.skills == ["scarf-template-author"])
|
||||||
|
#expect(inspection.manifest.config == nil)
|
||||||
|
#expect(inspection.cronJobs.isEmpty)
|
||||||
|
|
||||||
|
// Plan: empty config, empty cron, but one skill queued for install
|
||||||
|
// under the template's namespaced dir. The namespace path has to
|
||||||
|
// match what the uninstaller wipes — `skills/templates/<slug>` —
|
||||||
|
// or uninstall leaves orphan skill files.
|
||||||
|
let scratch = try ProjectTemplateServiceTests.makeTempDir()
|
||||||
|
defer { try? FileManager.default.removeItem(atPath: scratch) }
|
||||||
|
let plan = try service.buildPlan(inspection: inspection, parentDir: scratch)
|
||||||
|
#expect(plan.projectDir.hasSuffix("awizemann-template-author"))
|
||||||
|
#expect(plan.cronJobs.isEmpty)
|
||||||
|
#expect(plan.configSchema == nil)
|
||||||
|
#expect(plan.configValues.isEmpty)
|
||||||
|
#expect(plan.memoryAppendix == nil)
|
||||||
|
|
||||||
|
// The skill should land at
|
||||||
|
// `~/.hermes/skills/templates/awizemann-template-author/scarf-template-author/SKILL.md`
|
||||||
|
// — namespace dir + skill folder + SKILL.md. Anything else
|
||||||
|
// breaks Hermes's recursive discovery or the uninstaller's
|
||||||
|
// `rm -rf` on the namespace dir.
|
||||||
|
let namespaceDir = try #require(plan.skillsNamespaceDir)
|
||||||
|
#expect(namespaceDir.hasSuffix("/skills/templates/awizemann-template-author"))
|
||||||
|
#expect(plan.skillsFiles.count == 1)
|
||||||
|
let skillDest = try #require(plan.skillsFiles.first?.destinationPath)
|
||||||
|
#expect(skillDest.hasSuffix("/scarf-template-author/SKILL.md"))
|
||||||
|
#expect(skillDest.hasPrefix(namespaceDir))
|
||||||
|
|
||||||
|
// No-config templates deliberately skip the manifest cache —
|
||||||
|
// the dashboard's Configuration button only shows up when
|
||||||
|
// `.scarf/manifest.json` exists, so a skill-only template
|
||||||
|
// like this one correctly doesn't surface that button.
|
||||||
|
// (See ProjectTemplateService.buildPlan lines 198–227.)
|
||||||
|
#expect(plan.manifestCachePath == nil)
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve the example bundle path robustly. Unit-test working dirs
|
/// Resolve the example bundle path robustly. Unit-test working dirs
|
||||||
/// differ between `xcodebuild test` (project root) and an Xcode IDE
|
/// differ between `xcodebuild test` (project root) and an Xcode IDE
|
||||||
/// run (build-output dir), so we walk up from this source file until
|
/// run (build-output dir), so we walk up from this source file until
|
||||||
|
|||||||
+6
-1
@@ -45,7 +45,12 @@ need_builder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
need_ghpages() {
|
need_ghpages() {
|
||||||
[[ -d "$GHPAGES_DIR/.git" ]] || die "no gh-pages worktree at $GHPAGES_DIR
|
# `.git` is a directory in a regular clone but a pointer FILE in a
|
||||||
|
# `git worktree add` worktree — `-e` covers both. The earlier `-d`
|
||||||
|
# check falsely rejected worktrees, so the script's own error
|
||||||
|
# message told users to re-run `git worktree add` on a worktree
|
||||||
|
# that was already there and valid.
|
||||||
|
[[ -e "$GHPAGES_DIR/.git" ]] || die "no gh-pages worktree at $GHPAGES_DIR
|
||||||
Run: git worktree add .gh-pages-worktree gh-pages"
|
Run: git worktree add .gh-pages-worktree gh-pages"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Template Author — Agent Instructions
|
||||||
|
|
||||||
|
This project is a help surface for the `scarf-template-author` Hermes skill. The same instructions apply whether you're Claude Code, Cursor, Codex, Aider, or any other agent that reads `AGENTS.md`.
|
||||||
|
|
||||||
|
## What this project is
|
||||||
|
|
||||||
|
Two things:
|
||||||
|
|
||||||
|
1. A minimal dashboard (`.scarf/dashboard.json`) the user lands on after install. It's a Quick Start text widget + an empty list widget. The list is an optional scratchpad where you can log projects you've scaffolded for the user, giving them a running audit trail. That's nice-to-have, not mandatory.
|
||||||
|
2. A skill at `~/.hermes/skills/templates/awizemann-template-author/scarf-template-author/SKILL.md`. The skill is the real value — it teaches you how to interview the user and scaffold a new Scarf-compatible project.
|
||||||
|
|
||||||
|
## What this project is NOT
|
||||||
|
|
||||||
|
- Not a running service. No cron jobs, no background tasks, no secrets.
|
||||||
|
- Not a dashboard you need to keep updated. The dashboard is documentation; the only mutation worth doing is appending to the Scaffolded Projects list after you scaffold something.
|
||||||
|
|
||||||
|
## When the user asks to create a Scarf project
|
||||||
|
|
||||||
|
The primary trigger. Phrases that should activate the full scaffolding flow:
|
||||||
|
|
||||||
|
- "Create a new Scarf project that …"
|
||||||
|
- "Scaffold a dashboard for …"
|
||||||
|
- "Set up a project to watch / track / report on …"
|
||||||
|
- "Help me author a Scarf template."
|
||||||
|
- "Build me a project that runs daily and …"
|
||||||
|
|
||||||
|
When you hear those:
|
||||||
|
|
||||||
|
1. Load the skill at `~/.hermes/skills/templates/awizemann-template-author/scarf-template-author/SKILL.md` and follow its interview flow. Do not improvise — the skill encodes the specific invariants Scarf enforces (widget types, field-type constraints, the `{{PROJECT_DIR}}` token, the paused-on-install cron rule, the secret-fields-have-no-defaults rule).
|
||||||
|
2. Scaffold into a directory the user picks. Use absolute paths.
|
||||||
|
3. After writing files, tell the user to register the project: click **+** in Scarf's Projects sidebar and pick the directory. Do not try to edit `~/.hermes/scarf/projects.json` yourself — Scarf reloads the registry on its own and the UI path is safer.
|
||||||
|
4. Optionally append to the Scaffolded Projects list in this project's `dashboard.json` so the user has a local record of what you've built for them. Preserve every other field in the dashboard as-is.
|
||||||
|
|
||||||
|
## When the user asks reference questions
|
||||||
|
|
||||||
|
If the user asks something like "what widget types does Scarf support?" or "how do I add a secret field?", you don't need to scaffold anything — answer inline. The skill's reference sections cover:
|
||||||
|
|
||||||
|
- The seven widget types (`stat`, `progress`, `text`, `table`, `chart`, `list`, `webview`) and their required fields.
|
||||||
|
- The seven config field types (`string`, `text`, `number`, `bool`, `enum`, `list`, `secret`) and their constraint keys.
|
||||||
|
- The `AGENTS.md` contract that every scaffolded project should honour.
|
||||||
|
|
||||||
|
Point them at the skill file if they want to read it directly. It's ~400 lines of structured markdown.
|
||||||
|
|
||||||
|
## What not to do
|
||||||
|
|
||||||
|
- Don't scaffold without asking the user where the project should live. The interview always asks for a parent directory.
|
||||||
|
- Don't register secrets in `<project>/.scarf/config.json`. Secret field values go through the macOS Keychain at install time; `config.json` stores `keychain://…` URIs, never plaintext. A scaffolded project that hasn't been installed yet has no secrets on disk at all.
|
||||||
|
- Don't claim dashboard widget titles the cron job doesn't actually update. The scaffolded `AGENTS.md` is a contract — if it says "the cron updates Sites Up / Sites Down", the cron prompt must match.
|
||||||
|
- Don't skip `{{PROJECT_DIR}}` token substitution in cron prompts. Hermes doesn't set a CWD for cron runs, so relative paths resolve against the agent's own dir — the installer swaps `{{PROJECT_DIR}}` for the absolute project path at install time.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- `SKILL.md` at `~/.hermes/skills/templates/awizemann-template-author/scarf-template-author/SKILL.md` — the full scaffolding playbook.
|
||||||
|
- [Project Templates wiki page](https://github.com/awizemann/scarf/wiki/Project-Templates) — user-facing docs.
|
||||||
|
- [`awizemann/site-status-checker`](https://awizemann.github.io/scarf/templates/awizemann-site-status-checker/) — a complete working example covering dashboard stats, a configurable list, a cron job, a Site-tab webview, and a full AGENTS.md contract. Read it when you're unsure how a piece should look.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Scarf Template Author
|
||||||
|
|
||||||
|
A Hermes skill that teaches your agent how to scaffold a new Scarf project — and, because Scarf's `.scarftemplate` format is symmetric with a live project on disk, how to shape it so you can publish it to the catalog later if you want.
|
||||||
|
|
||||||
|
## What you get
|
||||||
|
|
||||||
|
Installing this template drops a skill at `~/.hermes/skills/templates/awizemann-template-author/scarf-template-author/SKILL.md` and a minimal "how to use" project in a folder of your choice. Every agent that reads the standard `~/.hermes/skills/` directory — Claude Code, Cursor, Codex, Aider, and the rest of the [agents.md](https://agents.md/) family — picks the skill up automatically.
|
||||||
|
|
||||||
|
## How to use it
|
||||||
|
|
||||||
|
After install, open your agent in any directory and say something like:
|
||||||
|
|
||||||
|
- *"Create a new Scarf project that watches the number of open PRs in my GitHub repo."*
|
||||||
|
- *"Scaffold a Scarf dashboard that tracks daily focus time from my Toggl logs."*
|
||||||
|
- *"Set up a project that runs a cron job to summarise my inbox each morning."*
|
||||||
|
- *"Help me author a Scarf template I can share."*
|
||||||
|
|
||||||
|
The agent will ask four or five questions (purpose, data source, cadence, what to display, any secrets) and then write:
|
||||||
|
|
||||||
|
- `<your-dir>/.scarf/dashboard.json`
|
||||||
|
- `<your-dir>/.scarf/manifest.json` — only if you're going to use a configuration form or want to export later
|
||||||
|
- `<your-dir>/AGENTS.md`
|
||||||
|
- `<your-dir>/README.md`
|
||||||
|
- Optionally a cron job registered via `hermes cron create` (always created paused — you enable it from Scarf's Cron sidebar when ready).
|
||||||
|
|
||||||
|
When it's done, click **+** in Scarf's Projects sidebar and pick the directory. Your dashboard appears. Iterate on it by asking your agent to tweak widgets or add fields.
|
||||||
|
|
||||||
|
## Turning a local project into a shareable template
|
||||||
|
|
||||||
|
Once you're happy with the result, Scarf → Projects → Templates → *Export "<name>" as Template…* produces a `.scarftemplate` anyone can install. The exporter carries the configuration *schema* but never your filled-in values — so your secrets and personal settings stay local.
|
||||||
|
|
||||||
|
## About this template's own dashboard
|
||||||
|
|
||||||
|
The installed project itself is tiny — a single Quick Start text widget and an empty list widget meant to serve as a scratchpad for tracking which scaffolded projects you've created. Its only purpose is to give you a place to land after install and a reminder of the trigger phrases above. The real value is the skill.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- [Project Templates wiki page](https://github.com/awizemann/scarf/wiki/Project-Templates) — full spec + troubleshooting.
|
||||||
|
- [`awizemann/site-status-checker`](https://awizemann.github.io/scarf/templates/awizemann-site-status-checker/) — a complete, non-trivial example the skill studies and references.
|
||||||
|
- Dashboard / configuration schemas are Swift-authoritative at `scarf/scarf/Core/Models/ProjectDashboard.swift` and `scarf/scarf/Core/Models/TemplateConfig.swift` in the Scarf repo.
|
||||||
|
|
||||||
|
## What this template intentionally is not
|
||||||
|
|
||||||
|
- Not an archetype picker. v1 is blank-slate conversational; pre-baked starters (`monitor`, `dev-dashboard`, `personal-log`, etc.) may land in v1.1 once we see what shapes people ask for most often.
|
||||||
|
- Not a graphical wizard. The conversational agent path is strictly richer than a fixed form, and dogfoods Scarf's agent-first philosophy.
|
||||||
|
- Not a remote-scaffolding tool. It writes files into a directory on the machine where the agent runs; pair with Scarf's remote-server mode if you want to scaffold onto another box.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"title": "Template Author",
|
||||||
|
"description": "A Hermes skill that helps your agent scaffold new Scarf projects — ask in chat, answer a short interview, and land a working dashboard with the right shape to export as a .scarftemplate later. The Scaffolded Projects list below grows as you use the skill.",
|
||||||
|
"theme": { "accent": "blue" },
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"title": "Quick Start",
|
||||||
|
"columns": 1,
|
||||||
|
"widgets": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"title": "Ask your agent",
|
||||||
|
"format": "markdown",
|
||||||
|
"content": "**This project gives you a skill, not a service.** There are no cron jobs running, no dashboards to maintain. The real value lives at `~/.hermes/skills/templates/awizemann-template-author/scarf-template-author/SKILL.md`.\n\n**Trigger phrases** your agent listens for:\n\n- *\"Create a new Scarf project that watches …\"*\n- *\"Scaffold a dashboard to track …\"*\n- *\"Set up a project that runs a daily check on …\"*\n- *\"Help me author a Scarf template.\"*\n\nThe agent will interview you (purpose → data source → cadence → widgets → config → secrets), write `<your-dir>/.scarf/dashboard.json`, `<your-dir>/.scarf/manifest.json`, `<your-dir>/AGENTS.md`, and `<your-dir>/README.md`, then tell you to click **+** in Scarf's Projects sidebar to register the directory.\n\nWhen you're happy with the result, **Projects → Templates → Export** turns it into a `.scarftemplate` you can share.\n\nSee the [Project Templates wiki page](https://github.com/awizemann/scarf/wiki/Project-Templates) for the full spec."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Scaffolded Projects",
|
||||||
|
"columns": 1,
|
||||||
|
"widgets": [
|
||||||
|
{
|
||||||
|
"type": "list",
|
||||||
|
"title": "Projects this skill has built for you",
|
||||||
|
"items": [
|
||||||
|
{ "text": "Nothing yet — ask your agent to scaffold a project and it'll optionally log entries here.", "status": "pending" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
---
|
||||||
|
name: scarf-template-author
|
||||||
|
description: Scaffold a new Scarf project — dashboard, optional configuration schema, optional cron job, and AGENTS.md — from a short conversational interview with the user. Output is immediately usable locally and cleanly exportable as a .scarftemplate bundle.
|
||||||
|
version: 1.0.0
|
||||||
|
author: Alan Wizemann
|
||||||
|
license: MIT
|
||||||
|
platforms: [macos]
|
||||||
|
metadata:
|
||||||
|
hermes:
|
||||||
|
tags: [Scarf, templates, scaffolding, dashboard, authoring]
|
||||||
|
homepage: https://github.com/awizemann/scarf/wiki/Project-Templates
|
||||||
|
prerequisites:
|
||||||
|
commands: [hermes]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scarf Template Author
|
||||||
|
|
||||||
|
Scaffold a new Scarf-compatible project from a conversational interview. The output is both (a) a working project on disk the user can register with Scarf and use immediately, and (b) correctly shaped to be exported as a `.scarftemplate` bundle via Scarf's Export flow later.
|
||||||
|
|
||||||
|
## When to invoke this skill
|
||||||
|
|
||||||
|
Activate when the user says things like:
|
||||||
|
|
||||||
|
- *"Create a new Scarf project that watches / tracks / reports on …"*
|
||||||
|
- *"Scaffold a dashboard for …"*
|
||||||
|
- *"Set up a project that runs a daily check on …"*
|
||||||
|
- *"Help me author a Scarf template."*
|
||||||
|
- *"Build me a Scarf project to monitor …"*
|
||||||
|
|
||||||
|
Do **not** activate for pure reference questions like *"what widget types does Scarf support?"* or *"how does Scarf handle secrets?"* — answer those inline from the reference sections below.
|
||||||
|
|
||||||
|
Also do not activate when the user explicitly wants to edit an existing project's dashboard — that's a plain file edit, not a scaffold.
|
||||||
|
|
||||||
|
## How a Scarf project is shaped on disk
|
||||||
|
|
||||||
|
A Scarf project is just a directory registered in `~/.hermes/scarf/projects.json`. For Scarf to render a useful dashboard and for the project to be exportable as a `.scarftemplate`, it needs these files at minimum:
|
||||||
|
|
||||||
|
```
|
||||||
|
<project>/
|
||||||
|
├── .scarf/
|
||||||
|
│ ├── dashboard.json # REQUIRED for dashboard rendering
|
||||||
|
│ └── manifest.json # OPTIONAL — required only if the project declares a config schema or you want to export cleanly
|
||||||
|
├── AGENTS.md # Cross-agent instructions (agents.md standard) — ship this for every project
|
||||||
|
└── README.md # User-facing explanation
|
||||||
|
```
|
||||||
|
|
||||||
|
If the project will have a scheduled job, ALSO register a cron entry via `hermes cron create`. For an exportable bundle, also author `cron/jobs.json` in the staging directory — that's where Scarf's exporter will pick jobs up from.
|
||||||
|
|
||||||
|
Secrets never land in `dashboard.json` or `config.json`. At install time, Scarf routes secret-type config values to the macOS Keychain; `config.json` stores `keychain://service/account` URIs. When scaffolding from scratch (no install), the user either manages secrets via the post-install Configuration editor after export, or stashes them in their `~/.hermes/config.yaml` if they're Hermes-level secrets rather than project-level.
|
||||||
|
|
||||||
|
## The interview
|
||||||
|
|
||||||
|
Ask these questions in order. Don't batch. Each answer shapes the next question.
|
||||||
|
|
||||||
|
### 1. Purpose and data source
|
||||||
|
|
||||||
|
- *"In one sentence — what does this project do?"*
|
||||||
|
- *"Where does its data come from? Files, a URL, a shell command's output, an API call, a database, a spreadsheet?"*
|
||||||
|
|
||||||
|
Goal: figure out whether the project is **passive** (user maintains some files, dashboard reflects them), **pull-based** (we fetch from an HTTP endpoint or CLI tool on a schedule), or **push-based** (something external writes to a file we watch).
|
||||||
|
|
||||||
|
### 2. Refresh cadence
|
||||||
|
|
||||||
|
- *"How often should it refresh? Every hour? Daily? Weekly? Only when I ask?"*
|
||||||
|
|
||||||
|
If "only when I ask" → no cron job; user invokes the agent manually. If any scheduled cadence → cron job.
|
||||||
|
|
||||||
|
Map to cron expressions:
|
||||||
|
- Every hour: `0 * * * *`
|
||||||
|
- Daily at 9 AM: `0 9 * * *`
|
||||||
|
- Weekly Monday 9 AM: `0 9 * * 1`
|
||||||
|
- Every 15 minutes: `*/15 * * * *`
|
||||||
|
|
||||||
|
### 3. What the dashboard shows
|
||||||
|
|
||||||
|
Explain the seven widget types (see Widget Catalog below) in plain English, then ask which ones feel right. Offer concrete suggestions based on the purpose:
|
||||||
|
|
||||||
|
- Counting things (open PRs, failing tests, up/down sites) → `stat` widgets.
|
||||||
|
- A list of items with status → `list` with `text` + `status` per item.
|
||||||
|
- Time-series data → `chart` with `line` or `bar` type.
|
||||||
|
- Rows × columns of heterogeneous data → `table`.
|
||||||
|
- A live URL (useful for monitoring a site) → `webview`. **Including a webview widget exposes a Site tab** next to the Dashboard tab — worth noting to the user.
|
||||||
|
- A progress bar for something with a clear 0-to-N scale → `progress`.
|
||||||
|
- Static help / markdown → `text` with `format: "markdown"`.
|
||||||
|
|
||||||
|
### 4. Configuration needs
|
||||||
|
|
||||||
|
- *"Does this project need anything configurable by the user — URLs to watch, API tokens, thresholds, a list of accounts?"*
|
||||||
|
|
||||||
|
If yes → design a config schema. Fields map to seven types (see Config Schema Design below). Remember: **secret fields never have defaults**; that's a hard validator rule.
|
||||||
|
|
||||||
|
If no → skip `.scarf/manifest.json`; the project works but won't have a Configuration form.
|
||||||
|
|
||||||
|
### 5. Target agents
|
||||||
|
|
||||||
|
- *"Which agents will operate this project? Just Claude Code? Also Cursor / Codex / Aider / other?"*
|
||||||
|
|
||||||
|
For v1 just write `AGENTS.md` — every modern agent reads it, and if you need a specific shim (CLAUDE.md, GEMINI.md, .cursorrules), add it as a symlink to AGENTS.md so content stays in sync.
|
||||||
|
|
||||||
|
## Widget Catalog (JSON shapes)
|
||||||
|
|
||||||
|
All widgets require `type` and `title`. Type-specific fields:
|
||||||
|
|
||||||
|
### `stat` — single metric
|
||||||
|
```json
|
||||||
|
{ "type": "stat", "title": "Sites Up", "value": 0,
|
||||||
|
"icon": "checkmark.circle.fill", "color": "green", "subtitle": "responded 2xx/3xx" }
|
||||||
|
```
|
||||||
|
`value` accepts number OR string (`WidgetValue` enum). `icon` is an SF Symbol name. `color` is one of: `green`, `red`, `blue`, `orange`, `yellow`, `purple`, `gray`.
|
||||||
|
|
||||||
|
### `progress` — 0.0 to 1.0 progress bar
|
||||||
|
```json
|
||||||
|
{ "type": "progress", "title": "Test Coverage", "value": 0.72, "label": "72% of statements" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `text` — markdown or plain text block
|
||||||
|
```json
|
||||||
|
{ "type": "text", "title": "Quick Start", "format": "markdown",
|
||||||
|
"content": "**1.** Click + in the Projects sidebar.\n\n**2.** ..." }
|
||||||
|
```
|
||||||
|
`format` is `"markdown"` or `"plain"`.
|
||||||
|
|
||||||
|
### `table` — columns × rows of strings
|
||||||
|
```json
|
||||||
|
{ "type": "table", "title": "Failing Tests",
|
||||||
|
"columns": ["Test", "Duration", "Last Passed"],
|
||||||
|
"rows": [["testFoo", "4.2s", "Apr 20"], ["testBar", "0.9s", "Apr 18"]] }
|
||||||
|
```
|
||||||
|
Every row MUST have the same length as `columns`.
|
||||||
|
|
||||||
|
### `chart` — line / bar / area / pie with series
|
||||||
|
```json
|
||||||
|
{ "type": "chart", "title": "Requests / day", "chartType": "line",
|
||||||
|
"xLabel": "Date", "yLabel": "Count",
|
||||||
|
"series": [{
|
||||||
|
"name": "staging",
|
||||||
|
"color": "blue",
|
||||||
|
"data": [{"x": "Apr 20", "y": 142}, {"x": "Apr 21", "y": 189}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`chartType` is `"line"`, `"bar"`, `"area"`, or `"pie"`.
|
||||||
|
|
||||||
|
### `list` — items with optional status badge
|
||||||
|
```json
|
||||||
|
{ "type": "list", "title": "Watched Sites",
|
||||||
|
"items": [
|
||||||
|
{ "text": "https://example.com", "status": "up" },
|
||||||
|
{ "text": "https://example.org", "status": "down" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`status` values: `"up"`, `"down"`, `"pending"`, `"ok"`, `"warn"`, `"error"` — render as coloured badges.
|
||||||
|
|
||||||
|
### `webview` — embedded live URL
|
||||||
|
```json
|
||||||
|
{ "type": "webview", "title": "First Watched Site",
|
||||||
|
"url": "https://awizemann.github.io/scarf/", "height": 420 }
|
||||||
|
```
|
||||||
|
**Important:** including any `webview` widget in a dashboard exposes a **Site** tab next to the Dashboard tab in the project view. Useful for templates that watch something renderable. The agent can update `url` on cron runs to keep the Site tab in sync with config (e.g., set it to `values.sites[0]`).
|
||||||
|
|
||||||
|
## Config Schema Design
|
||||||
|
|
||||||
|
If the project needs user-configurable values, design a schema. Put it in `<project>/.scarf/manifest.json` with this shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"id": "author/project",
|
||||||
|
"name": "My Project",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Short one-liner.",
|
||||||
|
"contents": { "dashboard": true, "agentsMd": true, "config": 2 },
|
||||||
|
"config": {
|
||||||
|
"schema": [
|
||||||
|
{ "key": "sites", "type": "list", "itemType": "string", "label": "Sites",
|
||||||
|
"required": true, "minItems": 1, "maxItems": 25,
|
||||||
|
"default": ["https://example.com"] },
|
||||||
|
{ "key": "api_token", "type": "secret", "label": "API Token", "required": true }
|
||||||
|
],
|
||||||
|
"modelRecommendation": {
|
||||||
|
"preferred": "claude-haiku-4",
|
||||||
|
"rationale": "Short-running, tool-light workload — haiku is plenty."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `contents.config` is the **count of schema fields**, not a boolean. In the example above it's `2` because there are two fields.
|
||||||
|
|
||||||
|
### Field types and constraints
|
||||||
|
|
||||||
|
| Type | Rendered as | Constraint keys |
|
||||||
|
|---|---|---|
|
||||||
|
| `string` | Text field | `pattern` (regex), `minLength`, `maxLength` |
|
||||||
|
| `text` | Multi-line editor | `minLength`, `maxLength` |
|
||||||
|
| `number` | Number field | `min`, `max` |
|
||||||
|
| `bool` | Toggle | — |
|
||||||
|
| `enum` | Segmented (≤4) / Dropdown (>4) | `options: [{value, label}]` (REQUIRED) |
|
||||||
|
| `list` | Repeatable rows | `itemType: "string"` (required), `minItems`, `maxItems` |
|
||||||
|
| `secret` | Password field, routes to Keychain | — |
|
||||||
|
|
||||||
|
Every field takes `key` (required), `label` (required), `description` (optional — markdown), `required` (bool), `default` (optional; type matches the field type).
|
||||||
|
|
||||||
|
### Writing good descriptions
|
||||||
|
|
||||||
|
Descriptions render inline with markdown support (bold, italic, code, links). Keep them short — a single line or two is ideal.
|
||||||
|
|
||||||
|
**Always use markdown link syntax for URLs**, never bare `https://…` — the Configuration sheet's inline text renderer doesn't word-break mid-URL, so a raw URL in a description will force that whole description's width to the URL's character length. Older Scarf versions clipped the sheet in that case; current versions wrap correctly, but the visible text is still cleaner with named links.
|
||||||
|
|
||||||
|
```json
|
||||||
|
// ✓ Good — short label, URL in the href
|
||||||
|
"description": "Token with `repo` scope. Get one [from the GitHub tokens page](https://github.com/settings/tokens)."
|
||||||
|
|
||||||
|
// ✗ Bad — raw URL bloats the visible text
|
||||||
|
"description": "Token with `repo` scope. Get one at https://github.com/settings/tokens"
|
||||||
|
```
|
||||||
|
|
||||||
|
Same rule for long file paths, API endpoints, or any other unbreakable token — wrap them in inline code (backticks) if they have to appear verbatim, and prefer markdown links otherwise.
|
||||||
|
|
||||||
|
### Hard rules
|
||||||
|
|
||||||
|
- **Secret fields MUST NOT have a `default`.** The validator rejects the manifest if they do — a default makes no sense because the Keychain entry doesn't exist yet at install time.
|
||||||
|
- **Enum fields MUST have non-empty `options`.**
|
||||||
|
- **List fields MUST have `itemType: "string"`** in v1 (only itemType supported).
|
||||||
|
- **Field keys MUST be unique** within a schema.
|
||||||
|
- **`schemaVersion` MUST be 2** when a `config` block is present; it stays 1 if there's no config.
|
||||||
|
- **`contents.config`** must equal the actual count of schema fields — a claim mismatch is rejected.
|
||||||
|
|
||||||
|
## Cron Job Design
|
||||||
|
|
||||||
|
If the project has a scheduled task, register a cron job via `hermes cron create` AND — if you expect the user to export this as a `.scarftemplate` — author a `cron/jobs.json` in the staging layout so the exporter picks it up.
|
||||||
|
|
||||||
|
### Staging shape (for exportable templates)
|
||||||
|
|
||||||
|
```
|
||||||
|
<project>/
|
||||||
|
├── .scarf/
|
||||||
|
├── AGENTS.md
|
||||||
|
├── README.md
|
||||||
|
└── cron/
|
||||||
|
└── jobs.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `cron/jobs.json` is:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Check site status",
|
||||||
|
"schedule": "0 9 * * *",
|
||||||
|
"prompt": "Read {{PROJECT_DIR}}/.scarf/config.json — get values.sites and values.timeout_seconds — then HTTP GET each URL with that timeout, write the results to {{PROJECT_DIR}}/status-log.md, and update {{PROJECT_DIR}}/.scarf/dashboard.json's stat widgets by title (Sites Up, Sites Down, Last Checked). Reply with a one-line summary."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gotchas
|
||||||
|
|
||||||
|
- **Hermes does not set a CWD when firing cron jobs.** Relative paths in the prompt resolve against wherever the Hermes process happens to be running, not the project. Always use `{{PROJECT_DIR}}` in the prompt — the installer substitutes the absolute path at install time. This is THE most common template-author mistake.
|
||||||
|
- **Cron jobs created by the installer start paused.** Their name is auto-prefixed with `[tmpl:<template-id>]`. The user enables them from Scarf's Cron sidebar when ready.
|
||||||
|
- **Registering a cron job for a user's local (non-exported) project:** run `hermes cron create --name "<descriptive name>" "<schedule>" "<prompt>"` directly, substituting the absolute `<project>` path for `{{PROJECT_DIR}}` yourself. Then `hermes cron pause <id>` so it doesn't run until the user opts in.
|
||||||
|
|
||||||
|
### Schedule quick reference
|
||||||
|
|
||||||
|
| Cadence | Expression |
|
||||||
|
|---|---|
|
||||||
|
| Every 15 minutes | `*/15 * * * *` |
|
||||||
|
| Hourly at :00 | `0 * * * *` |
|
||||||
|
| Daily at 9 AM | `0 9 * * *` |
|
||||||
|
| Weekly Monday 9 AM | `0 9 * * 1` |
|
||||||
|
| First of the month, 9 AM | `0 9 1 * *` |
|
||||||
|
|
||||||
|
## Writing the files
|
||||||
|
|
||||||
|
After the interview, write files in this order.
|
||||||
|
|
||||||
|
### Step 1 — confirm parent directory
|
||||||
|
|
||||||
|
Ask: *"Where should I create the project? Give me an absolute path — I'll make a `<project-name>` directory inside it."*
|
||||||
|
|
||||||
|
Make sure the parent exists and is writable. Make sure `<parent>/<project-name>` does NOT already exist. If it does, ask whether to pick a different name or bail.
|
||||||
|
|
||||||
|
### Step 2 — create the skeleton
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p <parent>/<project-name>/.scarf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3 — write `dashboard.json`
|
||||||
|
|
||||||
|
Use the Widget Catalog above. Always include:
|
||||||
|
|
||||||
|
- `version: 1`
|
||||||
|
- `title` (the project's display name)
|
||||||
|
- `description` (a one-liner shown under the title)
|
||||||
|
- `sections` (array; each has `title`, optional `columns` (1–4, default 3), `widgets`)
|
||||||
|
|
||||||
|
Keep section titles short. Group related widgets. First section is usually "Current Status" or similar with the key stats.
|
||||||
|
|
||||||
|
### Step 4 — write `manifest.json` (only if the project has a config schema)
|
||||||
|
|
||||||
|
Put the full manifest shape from Config Schema Design above. Use `schemaVersion: 2`, match `contents.config` to the actual field count, and ensure every secret field has no `default`.
|
||||||
|
|
||||||
|
If there's no config schema, skip this file — the project still works, it just won't have a Configuration button. You can add it later.
|
||||||
|
|
||||||
|
### Step 5 — write `AGENTS.md`
|
||||||
|
|
||||||
|
Every scaffolded project needs an `AGENTS.md` that covers:
|
||||||
|
|
||||||
|
- **Purpose** — what the project does.
|
||||||
|
- **Layout** — which files exist and what they're for.
|
||||||
|
- **Configuration** — if there's a config schema, document every field: what it's for, what valid values look like, what happens when it's missing.
|
||||||
|
- **Dashboard** — list every widget the cron job (if any) updates, by title. If the cron updates a webview widget's URL, document that explicitly.
|
||||||
|
- **Cron behaviour** — what the cron job does, what it reads, what it writes, what its exit criteria are.
|
||||||
|
- **Chat prompts** — common user questions and how to answer them (e.g., *"What's the status of my sites?"* → "read the top section of `status-log.md` and summarise").
|
||||||
|
- **What NOT to do** — e.g., *don't modify `.scarf/config.json` yourself; tell the user to open the Configuration button.*
|
||||||
|
|
||||||
|
Use `{{PROJECT_DIR}}` placeholders in AGENTS.md only if the template will be installed through the installer (which substitutes the token). For a hand-scaffolded local-only project, substitute the absolute path yourself — `{{PROJECT_DIR}}` only resolves at install time.
|
||||||
|
|
||||||
|
### Step 6 — write `README.md`
|
||||||
|
|
||||||
|
User-facing. Keep it short:
|
||||||
|
|
||||||
|
- One-paragraph purpose.
|
||||||
|
- How to install / first run (for an unexported project: "click + in Scarf's Projects sidebar").
|
||||||
|
- How to trigger the cron job manually (Cron sidebar → Run Now).
|
||||||
|
- A pointer at `AGENTS.md` for agents.
|
||||||
|
|
||||||
|
### Step 7 — register the cron job (if any)
|
||||||
|
|
||||||
|
For a local non-exported project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hermes cron create --name "<descriptive name>" "<schedule>" "<prompt with absolute project dir substituted>"
|
||||||
|
# Then pause it so it doesn't fire until the user's ready:
|
||||||
|
hermes cron pause <newly-created-job-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
Read the id back from `hermes cron list --json` or parse the create output.
|
||||||
|
|
||||||
|
For an exportable template (one you're staging in `templates/<author>/<name>/staging/`): just author `cron/jobs.json` — the installer registers + pauses at install time, and prefixes the name with `[tmpl:<id>]`.
|
||||||
|
|
||||||
|
### Step 8 — register the project with Scarf
|
||||||
|
|
||||||
|
Tell the user: *"I've written the files. Click the **+** button in Scarf's Projects sidebar and pick `<absolute-project-dir>`. The dashboard will appear."*
|
||||||
|
|
||||||
|
Do NOT edit `~/.hermes/scarf/projects.json` directly — Scarf owns that file and reloads it on its own. The UI path is safer.
|
||||||
|
|
||||||
|
### Step 9 (optional) — log to the Template Author project's list
|
||||||
|
|
||||||
|
If the user has the `awizemann/template-author` project installed (the one that shipped this skill), append an entry to its `dashboard.json`'s `Scaffolded Projects` list widget:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "text": "<absolute-project-dir> — <one-line purpose>", "status": "ok" }
|
||||||
|
```
|
||||||
|
|
||||||
|
This gives the user a running audit trail of everything you've scaffolded for them. Preserve every other field in the dashboard as-is.
|
||||||
|
|
||||||
|
## Testing your scaffold
|
||||||
|
|
||||||
|
### Minimum smoke test
|
||||||
|
|
||||||
|
1. Tell the user to click **+** in Scarf's Projects sidebar and pick the directory.
|
||||||
|
2. Dashboard appears — sanity check every widget renders correctly.
|
||||||
|
3. If there's a cron job: click the job in Scarf's Cron sidebar → **Run Now**. The agent executes the prompt; dashboard updates when it finishes.
|
||||||
|
|
||||||
|
### Configuration-form test (only if schema was declared)
|
||||||
|
|
||||||
|
To verify the Configuration form renders, you need to *install* the project as a template — scaffolded projects don't go through the installer, so the form never runs. Export the project first:
|
||||||
|
|
||||||
|
1. Projects → Templates → **Export "<name>" as Template…** → save the `.scarftemplate` somewhere.
|
||||||
|
2. Projects → Templates → **Install from File…** → pick the bundle → the Configure step should render the form you designed.
|
||||||
|
3. Cancel the install (the preview sheet has a Cancel button) — you just wanted to verify the form shape.
|
||||||
|
|
||||||
|
### Catalog validation (only if publishing)
|
||||||
|
|
||||||
|
If the user plans to submit this to the public catalog at `awizemann.github.io/scarf/templates/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From the repo root
|
||||||
|
./scripts/catalog.sh check
|
||||||
|
```
|
||||||
|
|
||||||
|
Validates every template in `templates/<author>/<name>/` against the Python validator — the same one the PR CI uses. Catches schema issues, claim mismatches, size violations, common secret patterns.
|
||||||
|
|
||||||
|
## Common pitfalls
|
||||||
|
|
||||||
|
Things to check before declaring the scaffold done:
|
||||||
|
|
||||||
|
- [ ] Every cron prompt uses `{{PROJECT_DIR}}` (for exported) OR an absolute path (for local-only). Relative paths will fail.
|
||||||
|
- [ ] `contents.config` in the manifest equals the actual field count. Claim mismatch = rejected.
|
||||||
|
- [ ] No `default` on any `secret` field.
|
||||||
|
- [ ] Every enum field has non-empty `options`.
|
||||||
|
- [ ] Every list field has `itemType: "string"`.
|
||||||
|
- [ ] Every table widget has rows of length equal to `columns`.
|
||||||
|
- [ ] Every webview widget has an https URL that renders something meaningful even pre-first-run (Scarf homepage is a decent placeholder).
|
||||||
|
- [ ] `dashboard.json` has `version: 1` at the top.
|
||||||
|
- [ ] `AGENTS.md` documents every config field, every updated widget, and the cron behaviour — the user relies on it as the source of truth when things drift.
|
||||||
|
- [ ] **No raw URLs in field descriptions.** Use `[link text](https://…)` markdown syntax instead — raw URLs read as long unbreakable tokens in the Configuration sheet. Same rule for long paths and other unbreakable strings; wrap in `` ` `` if they must appear verbatim.
|
||||||
|
|
||||||
|
## Reference — source of truth files
|
||||||
|
|
||||||
|
- **Dashboard widget schema** — `scarf/scarf/Core/Models/ProjectDashboard.swift` in the Scarf repo. If you need exact field types or defaults, read it.
|
||||||
|
- **Config schema + validation** — `scarf/scarf/Core/Models/TemplateConfig.swift` and `scarf/scarf/Core/Services/ProjectConfigService.swift`.
|
||||||
|
- **Exporter behaviour** — `scarf/scarf/Core/Services/ProjectTemplateExporter.swift`. Verifies what files the exporter will pick up from a live project and what it'll carry into a bundle.
|
||||||
|
- **Installer contract** — `scarf/scarf/Core/Services/ProjectTemplateInstaller.swift`. Verifies what `{{PROJECT_DIR}}` substitution covers and where installed files land.
|
||||||
|
- **Catalog validator** — `tools/build-catalog.py` in the Scarf repo. Run with `./scripts/catalog.sh check` for the same rules CI uses.
|
||||||
|
- **Worked example** — `templates/awizemann/site-status-checker/staging/` in the Scarf repo. Complete end-to-end: dashboard with stats + list + webview, a config schema with a list + a number, a cron job, an AGENTS.md that documents every moving part. Read it first whenever you're unsure how a piece should look.
|
||||||
|
- **User-facing docs** — [Project Templates wiki page](https://github.com/awizemann/scarf/wiki/Project-Templates).
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"id": "awizemann/template-author",
|
||||||
|
"name": "Scarf Template Author",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Install this to give your agent a skill that scaffolds new Scarf projects — dashboards, optional configuration schemas, cron jobs, and AGENTS.md — from a short conversational interview. Scaffolded projects are usable locally and cleanly exportable as .scarftemplate bundles.",
|
||||||
|
"minScarfVersion": "2.2.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Alan Wizemann",
|
||||||
|
"url": "https://github.com/awizemann"
|
||||||
|
},
|
||||||
|
"category": "developer-tools",
|
||||||
|
"tags": ["meta", "authoring", "skill", "scaffolding"],
|
||||||
|
"contents": {
|
||||||
|
"dashboard": true,
|
||||||
|
"agentsMd": true,
|
||||||
|
"skills": ["scarf-template-author"]
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -63,6 +63,37 @@
|
|||||||
"configurable"
|
"configurable"
|
||||||
],
|
],
|
||||||
"version": "1.1.0"
|
"version": "1.1.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"author": {
|
||||||
|
"name": "Alan Wizemann",
|
||||||
|
"url": "https://github.com/awizemann"
|
||||||
|
},
|
||||||
|
"bundleSha256": "bebc30551dc92717da96608bbdf448c5d7c47bdb66807037b139a242ef8c3b74",
|
||||||
|
"bundleSize": 14423,
|
||||||
|
"category": "developer-tools",
|
||||||
|
"config": null,
|
||||||
|
"contents": {
|
||||||
|
"agentsMd": true,
|
||||||
|
"dashboard": true,
|
||||||
|
"skills": [
|
||||||
|
"scarf-template-author"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Install this to give your agent a skill that scaffolds new Scarf projects \u2014 dashboards, optional configuration schemas, cron jobs, and AGENTS.md \u2014 from a short conversational interview. Scaffolded projects are usable locally and cleanly exportable as .scarftemplate bundles.",
|
||||||
|
"detailSlug": "awizemann-template-author",
|
||||||
|
"id": "awizemann/template-author",
|
||||||
|
"installUrl": "https://raw.githubusercontent.com/awizemann/scarf/main/templates/awizemann/template-author/template-author.scarftemplate",
|
||||||
|
"minHermesVersion": null,
|
||||||
|
"minScarfVersion": "2.2.0",
|
||||||
|
"name": "Scarf Template Author",
|
||||||
|
"tags": [
|
||||||
|
"meta",
|
||||||
|
"authoring",
|
||||||
|
"skill",
|
||||||
|
"scaffolding"
|
||||||
|
],
|
||||||
|
"version": "1.0.0"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user