@sableui/react 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +10 -3
- package/CHANGELOG.md +132 -0
- package/README.md +11 -4
- package/bin/sable.mjs +160 -18
- package/dist/activity-DT8VK9fx.d.ts +25 -0
- package/dist/adapters/github.d.ts +41 -0
- package/dist/adapters/github.js +24 -0
- package/dist/blocks/approval-block.js +30 -48
- package/dist/blocks/approval-block.meta.js +7 -8
- package/dist/blocks/index.d.ts +1 -1
- package/dist/components/activity-heatmap/activity-heatmap.js +420 -0
- package/dist/components/activity-heatmap/activity-heatmap.meta.js +142 -0
- package/dist/components/activity-heatmap/index.js +9 -0
- package/dist/components/code-block/code-block.js +5 -3
- package/dist/components/code-block/code-block.meta.js +6 -0
- package/dist/components/dialog/dialog.js +25 -5
- package/dist/components/dialog/dialog.meta.js +26 -5
- package/dist/components/media-image/media-image.js +3 -1
- package/dist/components/media-image/media-image.meta.js +15 -1
- package/dist/components/media-tile/media-tile.js +13 -1
- package/dist/components/media-tile/media-tile.meta.js +10 -0
- package/dist/components/page-header/page-header.js +9 -4
- package/dist/components/page-header/page-header.meta.js +26 -1
- package/dist/components/pill/pill.meta.js +15 -6
- package/dist/components/pill/pill.variants.js +1 -0
- package/dist/components/search-input/search-input.js +30 -5
- package/dist/components/search-input/search-input.meta.js +29 -4
- package/dist/components/section-header/section-header.js +27 -17
- package/dist/components/section-header/section-header.meta.js +27 -5
- package/dist/components/section-rail/index.js +7 -0
- package/dist/components/section-rail/section-rail.js +153 -0
- package/dist/components/section-rail/section-rail.meta.js +189 -0
- package/dist/components/sheet/sheet.js +15 -3
- package/dist/components/sheet/sheet.meta.js +2 -2
- package/dist/file-hashes.json +53 -39
- package/dist/icon-sets/reicon.js +23 -1
- package/dist/index.d.ts +364 -27
- package/dist/index.js +11 -1
- package/dist/layout/AspectBox.js +2 -5
- package/dist/layout/Bleed.js +10 -6
- package/dist/layout/Center.js +11 -3
- package/dist/layout/Columns.js +6 -3
- package/dist/layout/Grid.js +14 -6
- package/dist/layout/GridOverlay.js +7 -7
- package/dist/layout/Region.js +18 -3
- package/dist/layout/Row.js +27 -20
- package/dist/layout/Spacer.js +13 -2
- package/dist/layout/SplitPane.js +1 -1
- package/dist/layout/Stack.js +25 -18
- package/dist/layout/index.d.ts +57 -6
- package/dist/lib/chart/activity.js +32 -0
- package/dist/lib/chart/calendar.js +67 -0
- package/dist/lib/chart/chart-tooltip.js +2 -2
- package/dist/lib/chart/heat.js +55 -0
- package/dist/lib/focus-return.js +53 -0
- package/dist/lib/taxonomy.js +2 -0
- package/dist/lib/use-active-section.js +59 -0
- package/dist/styles.css +122 -114
- package/llms/blocks/approval-block.md +40 -55
- package/llms/components/activity-heatmap.md +133 -0
- package/llms/components/code-block.md +1 -0
- package/llms/components/dialog.md +34 -3
- package/llms/components/media-image.md +12 -0
- package/llms/components/media-tile.md +21 -0
- package/llms/components/page-header.md +16 -1
- package/llms/components/pill.md +24 -5
- package/llms/components/search-input.md +37 -4
- package/llms/components/section-header.md +28 -4
- package/llms/components/section-rail.md +101 -0
- package/llms/components/sheet.md +2 -2
- package/llms/layout/aspect-box.md +1 -1
- package/llms/layout/bleed.md +1 -1
- package/llms/layout/center.md +1 -1
- package/llms/layout/columns.md +1 -1
- package/llms/layout/grid.md +1 -1
- package/llms/layout/region.md +1 -1
- package/llms/layout/row.md +9 -1
- package/llms/layout/spacer.md +1 -1
- package/llms/layout/split-pane.md +1 -1
- package/llms/layout/stack.md +1 -1
- package/llms/tasks/auditing-an-app.md +31 -0
- package/llms/tasks/getting-started.md +10 -3
- package/llms/tasks/using-presets.md +1 -2
- package/llms-full.txt +516 -90
- package/llms.txt +5 -3
- package/package.json +6 -1
- package/registry.json +745 -46
- package/scripts/adoption.mjs +239 -0
- package/scripts/agent-setup.mjs +110 -18
- package/scripts/grade.mjs +17 -0
- package/scripts/wiring.mjs +2 -1
- package/skills/entry/SKILL.md +1 -1
- package/skills/sable/SKILL.md +33 -4
- package/skills/sable/agents/openai.yaml +1 -1
- package/targets/ios.json +18 -0
package/AGENTS.md
CHANGED
|
@@ -153,6 +153,11 @@ In a monorepo where Sable belongs to one workspace package, run setup in that
|
|
|
153
153
|
package with `--root` naming the repository root (for example
|
|
154
154
|
`pnpm --filter web exec sable setup --agent --root ../..`); the entries go
|
|
155
155
|
there and resolve Sable through the package.
|
|
156
|
+
Setup also adds a short managed block to the project's `AGENTS.md` and
|
|
157
|
+
`CLAUDE.md`, the instructions an agent reads on every task, so a UI request
|
|
158
|
+
goes through the skill even when it doesn't mention Sable. Only the text between
|
|
159
|
+
its `<!-- sable:begin -->` and `<!-- sable:end -->` markers is Sable's;
|
|
160
|
+
`--no-instructions` skips it.
|
|
156
161
|
Each entry resolves `skills/sable/SKILL.md` from the installed package, so package
|
|
157
162
|
upgrades also update the workflow. Other agents can read that file directly.
|
|
158
163
|
Use `$sable build <idea>`, `inspire <references>`, `audit <target>`,
|
|
@@ -160,9 +165,11 @@ Use `$sable build <idea>`, `inspire <references>`, `audit <target>`,
|
|
|
160
165
|
Claude Code or Cursor. `create` is a build alias. `fix` takes the short path for
|
|
161
166
|
one scoped defect and escalates to `polish` when the change grows. These are agent modes, not shell commands.
|
|
162
167
|
|
|
163
|
-
Upgrade with `pnpm
|
|
164
|
-
|
|
165
|
-
|
|
168
|
+
Upgrade with `pnpm exec sable upgrade`: it updates the package, then runs the
|
|
169
|
+
new version's `init`, which refreshes the skill entries and the instructions
|
|
170
|
+
block and checks the wiring. From 0.17 or earlier, which has no `upgrade`, run
|
|
171
|
+
`pnpm update @sableui/react --latest` and then `pnpm exec sable init` once.
|
|
172
|
+
Setup only replaces managed files that have not been locally edited; `--dry-run` checks without writing. Keep
|
|
166
173
|
project preferences in project instructions. Package installation alone does not
|
|
167
174
|
change agent settings. See [building-with-sable.md](./llms/tasks/building-with-sable.md),
|
|
168
175
|
[creating-a-preset.md](./llms/tasks/creating-a-preset.md), and
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,137 @@
|
|
|
1
1
|
# What's new
|
|
2
2
|
|
|
3
|
+
## 0.18.0
|
|
4
|
+
|
|
5
|
+
A year at a glance: the contribution graph, built so a phone, a keyboard and a
|
|
6
|
+
screen reader can all read it.
|
|
7
|
+
|
|
8
|
+
- **New: `ActivityHeatmap`.** A calendar heatmap: one value per day as a grid
|
|
9
|
+
of weeks by weekdays — the GitHub contribution graph, for any daily measure.
|
|
10
|
+
Pass days as `"YYYY-MM-DD"` with a value (or a date-to-value map); levels
|
|
11
|
+
come by quartile, as GitHub's do, unless a day brings its own. `categories`
|
|
12
|
+
colour days by source instead of intensity, and `mergeActivity` tags each day
|
|
13
|
+
with its source or `both`. The grid is a real `role="grid"` table with one
|
|
14
|
+
tab stop: arrows step a day or a week, each cell is named with its date and
|
|
15
|
+
value, and the tooltip shows on hover, focus and tap. It fills the width down
|
|
16
|
+
to `size`; narrower, the weeks scroll sideways and open on the latest, while
|
|
17
|
+
the weekday labels stay outside the scroller. Days are read as calendar days,
|
|
18
|
+
never instants, so no time zone shifts one. Prompted by a consumer site that
|
|
19
|
+
hand-built one from 371 buttons and broke on phones.
|
|
20
|
+
- **New: `@sableui/react/adapters/github`.** `fromGitHubGraphQL` and
|
|
21
|
+
`fromGitHubContributionsApi` turn GitHub's contribution data into heatmap
|
|
22
|
+
days. Pure transforms: fetch on your server and pass the result in.
|
|
23
|
+
- **New: `activityLevels` and `activityGrid`.** The level scale and the week
|
|
24
|
+
grid behind the heatmap, exported for a custom legend, a summary stat or
|
|
25
|
+
another layout.
|
|
26
|
+
- **`ChartTooltipContent` with no rows** draws just its label, so a caller's
|
|
27
|
+
own content sits on the standard tooltip card.
|
|
28
|
+
- **Layout primitives take `className` over their own layout.** Row, Stack,
|
|
29
|
+
Grid, Columns, Col, Center, Region, Page, Bleed, AspectBox, Spacer, SplitPane
|
|
30
|
+
and GridOverlay set `display`, direction, alignment, gap, inset, cap, ratio and
|
|
31
|
+
size inline, and an inline declaration beats every utility, so
|
|
32
|
+
`<Row className="max-lg:hidden">` or `hidden md:flex` silently did nothing.
|
|
33
|
+
The grid and split rules in `styles.css` were unlayered too, so `hidden` lost
|
|
34
|
+
to them whatever its specificity. Layout now lives in classes merged through
|
|
35
|
+
`cn()`, with prop values passed as CSS variables those classes read, and the
|
|
36
|
+
layout rules sit in `@layer components`. A utility in `className` wins at any
|
|
37
|
+
breakpoint (`max-md:flex-col`, `md:gap-6`, `lg:grid-cols-3`); the props work
|
|
38
|
+
as before. Migration: a `className` that used to be ignored on a primitive now
|
|
39
|
+
applies, and a test reading `style.gap` or `style.maxWidth` reads
|
|
40
|
+
`--sable-row-gap`, `--sable-region-max` and the like instead.
|
|
41
|
+
- **`PageHeader size="lg"`.** A display-size page title for an editorial page
|
|
42
|
+
(a personal site, a marketing page, an article index): `text-display-lg` over
|
|
43
|
+
a `text-body-lg` lede capped at the prose measure. `md`, the default, is
|
|
44
|
+
unchanged. Keep `lg` off app routes: display type stays out of product chrome.
|
|
45
|
+
- **`SectionHeader size="lg"`.** The major-section tier, `text-heading-2` over a
|
|
46
|
+
`text-body-md` description, for an editorial page's sections under a
|
|
47
|
+
`PageHeader size="lg"`. It stops at heading-2 because heading-1 is the page
|
|
48
|
+
title's, one per route. Actions stay centred on the title's first line at
|
|
49
|
+
every size.
|
|
50
|
+
- **`SearchInput` takes `size`** (`md` | `sm` | `xs`), on the same ladder as
|
|
51
|
+
Input, Select and Combobox. `sm` is the toolbar field on `--control-sm`, `xs`
|
|
52
|
+
the inset tool-panel field; the magnifier, padding and clear button scale with
|
|
53
|
+
it and stay centred. Before, it was fixed at 40px and matched nothing in a
|
|
54
|
+
toolbar.
|
|
55
|
+
- **`Pill size="lg"`.** A 32px chip on `--control-sm`, the toolbar height Button
|
|
56
|
+
`sm` and SearchInput `sm` share, so a filter row beside a search field is one
|
|
57
|
+
height in either density.
|
|
58
|
+
- **`MediaImage` and `MediaTile` take a crop focus.** `position` sets where the
|
|
59
|
+
image sits in its frame and takes any CSS `object-position` value
|
|
60
|
+
(`"35% 50%"`, `"top"`), beside `fit`. MediaTile forwards it to its cover.
|
|
61
|
+
- **Dialogs return focus to whatever opened them.** `Dialog`, `AlertDialog` and
|
|
62
|
+
`Sheet` record the element focused when `open` turns true and send focus back
|
|
63
|
+
to it on close, with or without a trigger. Before, a dialog opened from your
|
|
64
|
+
own button lost focus to the page whenever a field inside autofocused,
|
|
65
|
+
CommandMenu's included. An explicit `finalFocus` still wins.
|
|
66
|
+
- **`DialogContent keepMounted`.** Keeps a closed dialog's content mounted and
|
|
67
|
+
hidden, so a half-written draft survives Esc or a stray click and is still
|
|
68
|
+
there on reopen. Opt in for drafts the user may come back to.
|
|
69
|
+
- **Eight more icon names.** `briefcase`, `layers`, `microphone`, `video`,
|
|
70
|
+
`broadcast`, `newspaper`, `palette` and `gamepad` join the contract, mapped
|
|
71
|
+
in the Reicon set, for a portfolio that imported reicon-react directly for
|
|
72
|
+
its work, audio, video, live, press, design and games sections.
|
|
73
|
+
- **Setup writes the skill into the instructions every task reads.** A skill
|
|
74
|
+
loads only when a request names it, so "add a pricing page" in a wired app
|
|
75
|
+
could skip it: the agent saw Tailwind and a stylesheet, wrote its own Button
|
|
76
|
+
and Modal, and put Sable's tokens on them. `sable setup --agent` (and `sable
|
|
77
|
+
init`) now also writes a short managed block into `AGENTS.md` and
|
|
78
|
+
`CLAUDE.md` that sends every UI request through the skill. Only the text
|
|
79
|
+
between its `<!-- sable:begin -->` and `<!-- sable:end -->` markers is
|
|
80
|
+
Sable's; `--no-instructions` skips it. **Migration:** from 0.17, run
|
|
81
|
+
`pnpm update @sableui/react --latest` then `pnpm exec sable init` once; after
|
|
82
|
+
that, `sable upgrade` does both.
|
|
83
|
+
- **New: `sable upgrade`.** One command to update: installs the latest
|
|
84
|
+
`@sableui/react` with the project's package manager, then runs the new
|
|
85
|
+
version's `init`, so the skill entries, the instructions block and the
|
|
86
|
+
checks are the new release's. Safe on an app that already works.
|
|
87
|
+
- **"Move this app onto Sable" is a request the skill understands.** It checks
|
|
88
|
+
the app, shows the swaps once, asks, and moves them a screen at a time; it
|
|
89
|
+
never starts a new build. `init` says so when an app's UI barely uses Sable,
|
|
90
|
+
and its next step for a ready app is now a plain request, not mode syntax.
|
|
91
|
+
- **Adoption, in `sable check`, `sable grade` and `sable doctor`.** How much of
|
|
92
|
+
the UI composes Sable, which app-built components do by hand what a Sable
|
|
93
|
+
component already does (a `Modal` → Dialog, a `SearchBar` → SearchInput,
|
|
94
|
+
behaviour first), and what the app already stands on: nothing, its own
|
|
95
|
+
primitives, or another library. Never a failure. `check` lists the swaps and,
|
|
96
|
+
when adoption is low, makes "ask the user about moving onto Sable" the first
|
|
97
|
+
review step; `doctor` warns. The skill and `sable docs auditing-an-app` say
|
|
98
|
+
how to put the choice to the user, more firmly when the app has no
|
|
99
|
+
foundation of its own.
|
|
100
|
+
- **The skill triggers on any UI work in a Sable app.** Its description now
|
|
101
|
+
says so, even when the request doesn't mention Sable.
|
|
102
|
+
- **`sable init --hook`.** Adds the Claude Code Stop hook that runs `sable
|
|
103
|
+
check` before a task can end. Run in a terminal, init asks; `--no-hook`
|
|
104
|
+
doesn't.
|
|
105
|
+
- **`CodeBlock wrap`.** Wraps long lines instead of scrolling them, for prose
|
|
106
|
+
in a code face: a prompt to paste into an agent, a log line. Code keeps
|
|
107
|
+
scrolling.
|
|
108
|
+
|
|
109
|
+
## 0.17.0
|
|
110
|
+
|
|
111
|
+
Knowing where you are: a rail that follows a long read, and an approval
|
|
112
|
+
that shows an agent's call the way agent products do.
|
|
113
|
+
|
|
114
|
+
- **New: `SectionRail`.** A slim vertical rail of dots beside a long read —
|
|
115
|
+
a case study, an essay, a long landing page — one per section. A bar marks
|
|
116
|
+
the section being read and glides to the next as the page scrolls; hovering
|
|
117
|
+
or focusing a dot names it in one tooltip that travels between dots
|
|
118
|
+
(`TooltipGroup`). Picking a dot jumps there with the page's own
|
|
119
|
+
`scroll-behavior` (instant under reduced motion), and the bar goes straight
|
|
120
|
+
to the pick rather than stepping through every section in between. Pass
|
|
121
|
+
`root` when the content scrolls inside an element: only that element moves,
|
|
122
|
+
never the page around it. Links are real `#id` anchors, so it works without
|
|
123
|
+
script. Promoted from carldecaire.com's case-study rail.
|
|
124
|
+
- **New: `useActiveSection(ids, { offset, root })`.** The scroll spy behind it,
|
|
125
|
+
exported for a contents list you draw yourself. The current section is the
|
|
126
|
+
last whose top has crossed the reading line; at the end of the scroll the
|
|
127
|
+
last one in view wins, so a short closing section is still reachable.
|
|
128
|
+
- **`ApprovalBlock` asks the way agent products do.** It wraps the call's own
|
|
129
|
+
`ToolCall` — tool name and exact input, open on arrival — that turns to
|
|
130
|
+
Allowed or Denied once answered, and offers Deny / Always allow / Allow once.
|
|
131
|
+
The details table and the "for the rest of this chat" checkbox are gone;
|
|
132
|
+
`onApprove({ remember })` is unchanged, with `remember` now true for Always
|
|
133
|
+
allow. The example is an agent asking to run a command.
|
|
134
|
+
|
|
3
135
|
## 0.16.0
|
|
4
136
|
|
|
5
137
|
Five pieces of the calm, product-chrome look that AI and B2B tools share:
|
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ pnpm add @sableui/react
|
|
|
69
69
|
pnpm exec sable init # or: npx sable init — once the package is installed
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
`init` reads your package manager from the lockfile and your framework from the project. It installs what is missing (React, Tailwind and its build plugin), puts the CSS imports below in order, adds React dedupe and the Tailwind plugin to a Vite config, loads the fonts, mounts `IconProvider` in a Vite starter's entry, installs the agent skill (`sable setup --agent`), then runs `sable doctor`. Anything it cannot change safely — an unfamiliar config, a Next.js layout — it prints as the exact snippet and the file it belongs in. Vite and Next.js (App Router) get every step; other setups get the packages and the stylesheet. `--dry-run` shows every change first; `--no-agent` skips the skill. Already set up? `sable doctor --fix` applies the same fixes to whatever doctor flags.
|
|
72
|
+
`init` reads your package manager from the lockfile and your framework from the project. It installs what is missing (React, Tailwind and its build plugin), puts the CSS imports below in order, adds React dedupe and the Tailwind plugin to a Vite config, loads the fonts, mounts `IconProvider` in a Vite starter's entry, installs the agent skill and its block in `AGENTS.md` / `CLAUDE.md` (`sable setup --agent`), offers the Claude Code Stop hook when you run it in a terminal, then runs `sable doctor`. Anything it cannot change safely — an unfamiliar config, a Next.js layout — it prints as the exact snippet and the file it belongs in. Vite and Next.js (App Router) get every step; other setups get the packages and the stylesheet. `--dry-run` shows every change first; `--no-agent` skips the skill. Already set up? `sable doctor --fix` applies the same fixes to whatever doctor flags.
|
|
73
73
|
|
|
74
74
|
The steps below are what `init` does, for doing it by hand or checking its work.
|
|
75
75
|
|
|
@@ -213,6 +213,11 @@ In a monorepo where Sable belongs to one workspace package, run setup in that
|
|
|
213
213
|
package with `--root` naming the repository root (for example
|
|
214
214
|
`pnpm --filter web exec sable setup --agent --root ../..`); the entries go
|
|
215
215
|
there and resolve Sable through the package.
|
|
216
|
+
Setup also adds a short managed block to the project's `AGENTS.md` and
|
|
217
|
+
`CLAUDE.md`, the instructions an agent reads on every task, so a UI request
|
|
218
|
+
goes through the skill even when it doesn't mention Sable. Only the text between
|
|
219
|
+
its `<!-- sable:begin -->` and `<!-- sable:end -->` markers is Sable's;
|
|
220
|
+
`--no-instructions` skips it.
|
|
216
221
|
Each entry resolves `skills/sable/SKILL.md` from the installed package, so package
|
|
217
222
|
upgrades also update the workflow. Other agents can read that file directly.
|
|
218
223
|
Use `$sable build <idea>`, `inspire <references>`, `audit <target>`, or
|
|
@@ -225,9 +230,11 @@ server: `claude mcp add --transport http sable https://www.sableui.com/api/mcp`
|
|
|
225
230
|
and `docs`, give the CLI's answers from the published package, at the version
|
|
226
231
|
the agent passes or the latest. Once Sable is installed, the CLI is the default.
|
|
227
232
|
|
|
228
|
-
Upgrade with `pnpm
|
|
229
|
-
|
|
230
|
-
|
|
233
|
+
Upgrade with `pnpm exec sable upgrade`: it updates the package, then runs the
|
|
234
|
+
new version's `init`, which refreshes the skill entries and the instructions
|
|
235
|
+
block and checks the wiring. From 0.17 or earlier, which has no `upgrade`, run
|
|
236
|
+
`pnpm update @sableui/react --latest` and then `pnpm exec sable init` once.
|
|
237
|
+
Setup only replaces managed files that have not been locally edited; `--dry-run` checks without writing. Keep
|
|
231
238
|
project preferences in project instructions. Package installation alone does not
|
|
232
239
|
change agent settings. See [building-with-sable.md](./llms/tasks/building-with-sable.md),
|
|
233
240
|
[creating-a-preset.md](./llms/tasks/creating-a-preset.md), and
|
package/bin/sable.mjs
CHANGED
|
@@ -36,13 +36,15 @@
|
|
|
36
36
|
|
|
37
37
|
import { spawnSync } from "node:child_process"
|
|
38
38
|
import { appendFileSync, existsSync, readdirSync, readFileSync, statSync } from "node:fs"
|
|
39
|
+
import { createRequire } from "node:module"
|
|
39
40
|
import { dirname, join, relative, resolve } from "node:path"
|
|
40
41
|
import { fileURLToPath } from "node:url"
|
|
41
42
|
import { skeletonize } from "../scripts/skeleton.mjs"
|
|
42
43
|
import { claudeStopHook, installAgentSkill, installClaudeStopHook } from "../scripts/agent-setup.mjs"
|
|
44
|
+
import { adoptionReport, swapLines } from "../scripts/adoption.mjs"
|
|
43
45
|
import { patchChecks } from "../scripts/patches.mjs"
|
|
44
46
|
import { sourcesCheck } from "../scripts/sources.mjs"
|
|
45
|
-
import { detectFramework, detectPackageManager, fixWiring, fontsCheck, providerSnippet, tailwindBuildCheck } from "../scripts/wiring.mjs"
|
|
47
|
+
import { detectFramework, detectPackageManager, fixWiring, fontsCheck, installCommand, providerSnippet, tailwindBuildCheck } from "../scripts/wiring.mjs"
|
|
46
48
|
|
|
47
49
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
48
50
|
const PKG_ROOT = join(HERE, "..")
|
|
@@ -119,7 +121,7 @@ const COMMANDS = [
|
|
|
119
121
|
},
|
|
120
122
|
{
|
|
121
123
|
name: "setup",
|
|
122
|
-
summary: "Install package-backed skill entry files for coding agents, preserving local edits.",
|
|
124
|
+
summary: "Install package-backed skill entry files for coding agents, plus a short managed block in AGENTS.md / CLAUDE.md that sends every UI request through the skill, preserving local edits.",
|
|
123
125
|
arg: null,
|
|
124
126
|
flags: [
|
|
125
127
|
{ name: "--agent", value: null, describe: "Install skill entry files. Required." },
|
|
@@ -128,6 +130,7 @@ const COMMANDS = [
|
|
|
128
130
|
{ name: "--update", value: null, describe: "Refresh unchanged managed entries after a package upgrade." },
|
|
129
131
|
{ name: "--dry-run", value: null, describe: "Check and report the destination without writing files." },
|
|
130
132
|
{ name: "--hook", value: null, describe: "Also add a Claude Code Stop hook that runs `sable check` to .claude/settings.json (Claude targets only). Without it, setup prints the hook and writes nothing there." },
|
|
133
|
+
{ name: "--no-instructions", value: null, describe: "Skip the managed Sable block in AGENTS.md / CLAUDE.md. Without that block an agent uses the skill only when a request names it." },
|
|
131
134
|
],
|
|
132
135
|
returns: ["setup.agent"],
|
|
133
136
|
},
|
|
@@ -249,7 +252,7 @@ const COMMANDS = [
|
|
|
249
252
|
},
|
|
250
253
|
{
|
|
251
254
|
name: "doctor",
|
|
252
|
-
summary: "Check this project's Sable wiring. Exits 1 on any failure.",
|
|
255
|
+
summary: "Check this project's Sable wiring, and warn when its UI barely uses Sable (adoption). Exits 1 on any wiring failure.",
|
|
253
256
|
arg: null,
|
|
254
257
|
flags: [
|
|
255
258
|
{ name: "--fix", value: null, describe: "Apply the fixes that need no judgment (imports in order, @source, missing packages, Vite dedupe and Tailwind plugin, fonts, IconProvider in a starter entry), print the rest, then check again." },
|
|
@@ -265,10 +268,26 @@ const COMMANDS = [
|
|
|
265
268
|
flags: [
|
|
266
269
|
{ name: "--dry-run", value: null, describe: "Report every change without writing or installing anything." },
|
|
267
270
|
{ name: "--no-agent", value: null, describe: "Skip the agent skill (sable setup --agent)." },
|
|
271
|
+
{ name: "--no-instructions", value: null, describe: "Install the skill but skip its managed block in AGENTS.md / CLAUDE.md." },
|
|
272
|
+
{ name: "--hook", value: null, describe: "Also add the Claude Code Stop hook that runs `sable check` before a task can end. In a terminal, init asks; elsewhere it only prints the hook." },
|
|
273
|
+
{ name: "--no-hook", value: null, describe: "Don't ask about the Stop hook." },
|
|
268
274
|
{ name: "--no-install", value: null, describe: "Print install commands instead of running them." },
|
|
269
275
|
],
|
|
270
276
|
returns: ["init"],
|
|
271
277
|
},
|
|
278
|
+
{
|
|
279
|
+
name: "upgrade",
|
|
280
|
+
summary: "Update @sableui/react to the latest release, then run the new version's init: skill entries and the AGENTS.md / CLAUDE.md block refreshed, wiring checked, adoption reported. Safe on an app that is already set up.",
|
|
281
|
+
arg: null,
|
|
282
|
+
flags: [
|
|
283
|
+
{ name: "--dry-run", value: null, describe: "Print the update command and preview init's changes without installing or writing anything." },
|
|
284
|
+
{ name: "--no-install", value: null, describe: "Skip the package update (you updated it yourself); just run the installed version's init." },
|
|
285
|
+
{ name: "--hook", value: null, describe: "Passed to init: add the Claude Code Stop hook." },
|
|
286
|
+
{ name: "--no-hook", value: null, describe: "Passed to init: don't ask about the Stop hook." },
|
|
287
|
+
{ name: "--no-instructions", value: null, describe: "Passed to init: skip the AGENTS.md / CLAUDE.md block." },
|
|
288
|
+
],
|
|
289
|
+
returns: ["init"],
|
|
290
|
+
},
|
|
272
291
|
{
|
|
273
292
|
name: "grade",
|
|
274
293
|
summary: "Static contract checks and evidence review. Original compositions carry no penalty; no automatic taste score.",
|
|
@@ -358,6 +377,7 @@ const ERROR_CODES = [
|
|
|
358
377
|
{ code: "ERR_PRESET_CONTRAST", when: "A label on a brand or status fill (primary, sidebar-primary, highlight, destructive, success, tone-warning, tone-info) misses 4.5:1 or APCA Lc 60. Apply the suggested pair, or record an `exceptions` entry with a reason." },
|
|
359
378
|
{ code: "ERR_AGENT_SETUP", when: "The packaged skill is missing, the destination is linked, or existing local edits would be overwritten." },
|
|
360
379
|
{ code: "ERR_INIT", when: "sable init ran outside an app root (no package.json)." },
|
|
380
|
+
{ code: "ERR_UPGRADE", when: "sable upgrade ran where @sableui/react is not a dependency, or the package manager's update failed." },
|
|
361
381
|
{ code: "ERR_UNKNOWN_COMMAND", when: "The first argument is not a command." },
|
|
362
382
|
{ code: "ERR_MISSING_ARGUMENT", when: "A command that requires an argument got none." },
|
|
363
383
|
{ code: "ERR_UNKNOWN_COMPONENT", when: "No component by that name. `suggestions` carries near matches." },
|
|
@@ -1471,6 +1491,21 @@ function doctorReport() {
|
|
|
1471
1491
|
)
|
|
1472
1492
|
}
|
|
1473
1493
|
|
|
1494
|
+
/* Adoption: wiring that holds says nothing about whether the app composes
|
|
1495
|
+
* Sable. An agent that never loaded the skill wires the stylesheet, uses the
|
|
1496
|
+
* tokens, and writes its own Button and Modal. A warning, never a failure:
|
|
1497
|
+
* check lists the swaps, and moving working screens is the user's call. */
|
|
1498
|
+
if (pkg && { ...pkg.dependencies, ...pkg.devDependencies }["@sableui/react"]) {
|
|
1499
|
+
const files = src
|
|
1500
|
+
.filter((f) => /\.(tsx|jsx)$/.test(f.path) && !/\.(test|spec|stories)\./.test(f.path))
|
|
1501
|
+
.map((f) => ({ rel: relative(CWD, f.path).replaceAll("\\", "/"), text: f.text }))
|
|
1502
|
+
const adoption = adoptionReport({ files, components: COMPONENTS, deps: { ...pkg.dependencies, ...pkg.devDependencies } })
|
|
1503
|
+
if (adoption.level === "none" || adoption.level === "low")
|
|
1504
|
+
add("warn", "adoption", `${adoption.summary} ${adoption.recommendation}`, "sable check src lists each swap; then ask your agent to /sable audit (or $sable audit) the app's use of Sable.")
|
|
1505
|
+
else if (adoption.level === "sable") add("pass", "adoption", adoption.summary)
|
|
1506
|
+
else if (adoption.level === "fresh") add("info", "adoption", adoption.summary)
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1474
1509
|
/* Tailwind in the build, and the theme's fonts: both fail as a page that
|
|
1475
1510
|
* renders, just not as Sable — unstyled, or in system fonts. */
|
|
1476
1511
|
if (pkg) {
|
|
@@ -1567,7 +1602,7 @@ function cmdDoctorFix() {
|
|
|
1567
1602
|
* app still has to mount. Each step is the code its own command runs, so init
|
|
1568
1603
|
* can never wire an app differently from what doctor checks.
|
|
1569
1604
|
*/
|
|
1570
|
-
function cmdInit() {
|
|
1605
|
+
async function cmdInit() {
|
|
1571
1606
|
if (!existsSync(join(CWD, "package.json")))
|
|
1572
1607
|
fail("ERR_INIT", `No package.json in ${CWD}.`, ["Run sable init from your app's root.", "Starting fresh: pnpm create vite@latest my-app --template react-ts, then pnpm add @sableui/react && pnpm exec sable init"])
|
|
1573
1608
|
const dryRun = flags.has("dry-run")
|
|
@@ -1582,26 +1617,57 @@ function cmdInit() {
|
|
|
1582
1617
|
try {
|
|
1583
1618
|
// update: an unchanged managed entry from an older Sable is refreshed;
|
|
1584
1619
|
// one with local edits still refuses, and is reported for the user.
|
|
1585
|
-
const setup = installAgentSkill({ root: CWD, project: CWD, packageRoot: PKG_ROOT, target: "all", update: true, dryRun })
|
|
1586
|
-
agent = { status: dryRun ? "would-apply" : "applied", destinations: setup.destinations, next: setup.next }
|
|
1620
|
+
const setup = installAgentSkill({ root: CWD, project: CWD, packageRoot: PKG_ROOT, target: "all", update: true, dryRun, instructions: !flags.has("no-instructions") })
|
|
1621
|
+
agent = { status: dryRun ? "would-apply" : "applied", destinations: setup.destinations, instructions: setup.instructions, instructionsChanged: setup.instructionsChanged, next: setup.next }
|
|
1587
1622
|
} catch (error) {
|
|
1588
1623
|
agent = { status: "manual", detail: error.message, fix: "sable setup --agent --dry-run" }
|
|
1589
1624
|
}
|
|
1590
1625
|
|
|
1626
|
+
/* The Stop hook changes how Claude Code behaves, so init never adds it on its
|
|
1627
|
+
* own. A person at a terminal is asked; an agent running init (no TTY) gets
|
|
1628
|
+
* the one-line offer in the output, and --hook adds it either way. */
|
|
1629
|
+
let hook = null
|
|
1630
|
+
if (agent.status === "applied" || agent.status === "would-apply") {
|
|
1631
|
+
let want = flags.has("hook")
|
|
1632
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY && !JSON_OUT && !dryRun
|
|
1633
|
+
if (!want && !flags.has("no-hook") && interactive) {
|
|
1634
|
+
const { createInterface } = await import("node:readline/promises")
|
|
1635
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
1636
|
+
const answer = await rl.question("Add a Claude Code Stop hook, so Claude can't end a task while `sable check` fails? [Y/n] ")
|
|
1637
|
+
rl.close()
|
|
1638
|
+
want = !/^\s*n/i.test(answer)
|
|
1639
|
+
}
|
|
1640
|
+
try {
|
|
1641
|
+
hook = want ? installClaudeStopHook({ root: CWD, project: CWD, dryRun }) : claudeStopHook({ root: CWD, project: CWD })
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
hook = { installed: false, error: error.message }
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1591
1647
|
const after = dryRun ? before : doctorReport()
|
|
1592
1648
|
verifyFixes(fixes, after)
|
|
1593
1649
|
const mounted = after.checks.find((c) => c.name === "icons")?.status === "pass"
|
|
1594
1650
|
const providers = { ...providerSnippet(framework), mounted }
|
|
1595
|
-
|
|
1651
|
+
// An app that is wired but built mostly without Sable needs the audit path
|
|
1652
|
+
// next, not a new build: `build` would start the product over.
|
|
1653
|
+
const lowAdoption = after.checks.some((c) => c.name === "adoption" && c.status === "warn")
|
|
1654
|
+
const data = { framework: { id: framework.id, label: framework.label }, packageManager, dryRun, fixes, agent, stopHook: hook, providers, doctor: after, next: lowAdoption ? EXISTING_APP_NEXT : (agent.next ?? null) }
|
|
1596
1655
|
|
|
1597
1656
|
if (JSON_OUT) process.stdout.write(`${JSON.stringify({ type: "init", data }, null, DENSE ? 0 : 1)}\n`)
|
|
1598
1657
|
else {
|
|
1599
1658
|
const agentText = {
|
|
1600
|
-
applied: () => `Agent skill: installed at ${agent.destinations.join(", ")}
|
|
1601
|
-
"would-apply": () => `Agent skill: would install at ${agent.destinations.join(", ")}
|
|
1659
|
+
applied: () => `Agent skill: installed at ${agent.destinations.join(", ")}.\n${instructionsText({ ...agent, dryRun })}`,
|
|
1660
|
+
"would-apply": () => `Agent skill: would install at ${agent.destinations.join(", ")}.\n${instructionsText({ ...agent, dryRun })}`,
|
|
1602
1661
|
skipped: () => "Agent skill: skipped (--no-agent).",
|
|
1603
1662
|
manual: () => `Agent skill: not installed — ${agent.detail}\n → ${agent.fix}`,
|
|
1604
1663
|
}[agent.status]()
|
|
1664
|
+
const hookText = !hook
|
|
1665
|
+
? ""
|
|
1666
|
+
: hook.error
|
|
1667
|
+
? `Stop hook: not added — ${hook.error}`
|
|
1668
|
+
: hook.installed
|
|
1669
|
+
? `Stop hook: ${dryRun ? "would add to" : hook.alreadyPresent ? "already in" : "added to"} ${hook.settingsFile}. Claude can't end a task while sable check fails.`
|
|
1670
|
+
: `Stop hook (optional): sable init --hook, or sable setup --agent --hook, makes Claude Code run sable check before it can end a task.`
|
|
1605
1671
|
const iconsFix = fixes.find((f) => f.check === "icons")
|
|
1606
1672
|
const providerText = mounted
|
|
1607
1673
|
? "Providers: <IconProvider> is mounted. MotionProvider and SableThemeProvider are optional: sable provider --list."
|
|
@@ -1617,13 +1683,14 @@ function cmdInit() {
|
|
|
1617
1683
|
"",
|
|
1618
1684
|
fixes.length ? `${dryRun ? "Would fix:" : "Fixes:"}\n${fixLines(fixes)}` : "Nothing to fix.\n",
|
|
1619
1685
|
agentText,
|
|
1686
|
+
...(hookText ? [hookText] : []),
|
|
1620
1687
|
"",
|
|
1621
1688
|
providerText,
|
|
1622
1689
|
"",
|
|
1623
1690
|
`Doctor:\n${doctorLines(after.checks)}`,
|
|
1624
1691
|
after.failed
|
|
1625
1692
|
? `[sable init] ✗ ${after.failed} failure(s) ${dryRun ? "now" : "left; the steps marked ! need you"}. Re-check with sable doctor.`
|
|
1626
|
-
: `[sable init] ✓ wiring holds. Next: ${agent.next ?? "sable docs building-with-sable"}`,
|
|
1693
|
+
: `[sable init] ✓ wiring holds. Next: ${lowAdoption ? EXISTING_APP_NEXT : (agent.next ?? "sable docs building-with-sable")}`,
|
|
1627
1694
|
"",
|
|
1628
1695
|
].join("\n"),
|
|
1629
1696
|
)
|
|
@@ -1632,6 +1699,54 @@ function cmdInit() {
|
|
|
1632
1699
|
process.exit(after.failed ? 1 : 0)
|
|
1633
1700
|
}
|
|
1634
1701
|
|
|
1702
|
+
const EXISTING_APP_NEXT = "most of this app's UI isn't built from Sable yet. Tell your agent \"move this app onto Sable\": it lists what to swap and asks before changing anything."
|
|
1703
|
+
|
|
1704
|
+
/* ── upgrade ────────────────────────────────────────────────────────────────── */
|
|
1705
|
+
|
|
1706
|
+
/**
|
|
1707
|
+
* The whole update in one command: the package, then the NEW version's init.
|
|
1708
|
+
* This process is the old CLI, so after installing it hands over to the
|
|
1709
|
+
* freshly installed bin rather than running its own init: the skill entries,
|
|
1710
|
+
* the instructions block and the checks must be the new release's.
|
|
1711
|
+
*/
|
|
1712
|
+
function cmdUpgrade() {
|
|
1713
|
+
let pkg
|
|
1714
|
+
try {
|
|
1715
|
+
pkg = JSON.parse(readFileSync(join(CWD, "package.json"), "utf8"))
|
|
1716
|
+
} catch {
|
|
1717
|
+
fail("ERR_UPGRADE", `No package.json in ${CWD}.`, ["Run sable upgrade from your app's root."])
|
|
1718
|
+
}
|
|
1719
|
+
const spec = pkg.dependencies?.["@sableui/react"] ?? pkg.devDependencies?.["@sableui/react"]
|
|
1720
|
+
if (!spec) fail("ERR_UPGRADE", "@sableui/react is not a dependency of this project.", ["pnpm add @sableui/react, then pnpm exec sable init"])
|
|
1721
|
+
const pm = detectPackageManager(CWD)
|
|
1722
|
+
const dryRun = flags.has("dry-run")
|
|
1723
|
+
// A tarball, link or workspace copy is not something "latest" can replace.
|
|
1724
|
+
const local = /^(?:file:|link:|workspace:|portal:|\.{0,2}\/)/.test(String(spec))
|
|
1725
|
+
const install = installCommand(pm, ["@sableui/react@latest"], { dev: !pkg.dependencies?.["@sableui/react"] })
|
|
1726
|
+
const say = (line) => { if (!JSON_OUT) process.stdout.write(`${line}\n`) }
|
|
1727
|
+
|
|
1728
|
+
if (flags.has("no-install")) say("sable upgrade — skipping the package update (--no-install).")
|
|
1729
|
+
else if (local) say(`sable upgrade — @sableui/react comes from ${spec}, which "latest" can't replace. Update that copy yourself; running the installed version's init.`)
|
|
1730
|
+
else if (dryRun) say(`sable upgrade — would run: ${install.join(" ")}`)
|
|
1731
|
+
else {
|
|
1732
|
+
say(`sable upgrade — ${install.join(" ")}`)
|
|
1733
|
+
const r = spawnSync(install[0], install.slice(1), { cwd: CWD, stdio: JSON_OUT ? "pipe" : "inherit", shell: process.platform === "win32" })
|
|
1734
|
+
if (r.status !== 0) fail("ERR_UPGRADE", `${install.join(" ")} exited ${r.status}.`, ["Fix the install, then run sable upgrade --no-install."])
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// The installed package's own CLI, resolved the way the skill resolves it.
|
|
1738
|
+
let bin = join(PKG_ROOT, "bin", "sable.mjs")
|
|
1739
|
+
try {
|
|
1740
|
+
const registry = createRequire(join(CWD, "package.json")).resolve("@sableui/react/registry.json")
|
|
1741
|
+
const installed = join(dirname(registry), "bin", "sable.mjs")
|
|
1742
|
+
if (existsSync(installed)) bin = installed
|
|
1743
|
+
} catch { /* not resolvable from here (a workspace copy): this CLI's init is the installed one */ }
|
|
1744
|
+
const passed = ["dry-run", "hook", "no-hook", "no-instructions", "json"].filter((f) => flags.has(f)).map((f) => `--${f}`)
|
|
1745
|
+
const r = spawnSync(process.execPath, [bin, "init", ...passed], { cwd: CWD, stdio: "inherit" })
|
|
1746
|
+
logInvocation(r.status ?? 1)
|
|
1747
|
+
process.exit(r.status ?? 1)
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1635
1750
|
function doctorLines(checks, indent = " ") {
|
|
1636
1751
|
const mark = { pass: "✓", warn: "!", fail: "✗", info: "·" }
|
|
1637
1752
|
return checks
|
|
@@ -1751,7 +1866,8 @@ function cmdCheck() {
|
|
|
1751
1866
|
details: doctorFailures.map((c) => `${c.name}: ${c.detail}${c.fix ? ` → ${c.fix}` : ""}`),
|
|
1752
1867
|
// Warnings and notes don't fail the check, but they are things to act on,
|
|
1753
1868
|
// and a passing check that printed nothing else hid them from the agent.
|
|
1754
|
-
|
|
1869
|
+
// Adoption is reported once, from grade, with its swaps.
|
|
1870
|
+
notes: doctor.checks.filter((c) => (c.status === "warn" || c.status === "info") && c.name !== "adoption").map((c) => `${c.name}: ${c.detail}${c.fix ? ` → ${c.fix}` : ""}`),
|
|
1755
1871
|
data: doctor,
|
|
1756
1872
|
})
|
|
1757
1873
|
|
|
@@ -1807,14 +1923,20 @@ function cmdCheck() {
|
|
|
1807
1923
|
/* App-owned surfaces, from grade. Never a failure: the input to the reuse
|
|
1808
1924
|
* inventory (finish step 6), which otherwise gets written from memory. */
|
|
1809
1925
|
const inventory = grade && grade.type !== "error" ? (grade.inventory ?? null) : null
|
|
1926
|
+
/* Adoption, from grade. Never a failure either: wiring can hold while the app
|
|
1927
|
+
* composes almost none of Sable, and the move onto it is the user's call. */
|
|
1928
|
+
const adoption = grade && grade.type !== "error" ? (grade.adoption ?? null) : null
|
|
1929
|
+
const lowAdoption = adoption && (adoption.level === "none" || adoption.level === "low")
|
|
1810
1930
|
|
|
1811
1931
|
const failed = checks.filter((c) => c.status !== "pass").map((c) => c.name)
|
|
1812
1932
|
// A build that still scans all of Sable ships every component's classes;
|
|
1813
1933
|
// trimming it is a finish step, because sources are only final once the app is.
|
|
1814
|
-
const manual =
|
|
1815
|
-
? [
|
|
1816
|
-
|
|
1817
|
-
|
|
1934
|
+
const manual = [
|
|
1935
|
+
...(lowAdoption ? [{ step: "Ask about moving onto Sable", how: `${adoption.recommendation} The swaps are listed under Adoption above; sable docs auditing-an-app ("Adoption") covers how to put the choice to the user.` }] : []),
|
|
1936
|
+
...MANUAL_REVIEW,
|
|
1937
|
+
...(doctor.checks.some((c) => c.name === "css:size") ? [{ step: "Ship only the CSS you use", how: "sable sources --write src/sable-sources.css, then @import it in place of the whole-package @source (about 32 KB of CSS gzipped down to what the app imports). Re-run it whenever you import something new from Sable; sable check fails while it is stale." }] : []),
|
|
1938
|
+
]
|
|
1939
|
+
const data = { paths, passed: !failed.length, failed, checks, inventory, adoption, manualReview: manual }
|
|
1818
1940
|
const MARK = { pass: "✓", fail: "✗", error: "✗" }
|
|
1819
1941
|
const MAX_DETAILS = 12
|
|
1820
1942
|
const summaryText = () =>
|
|
@@ -1840,6 +1962,16 @@ function cmdCheck() {
|
|
|
1840
1962
|
"",
|
|
1841
1963
|
].join("\n")
|
|
1842
1964
|
}
|
|
1965
|
+
const adoptionText = () => {
|
|
1966
|
+
if (!adoption || adoption.level === "empty" || (adoption.level === "sable" && !adoption.swaps.length)) return ""
|
|
1967
|
+
return [
|
|
1968
|
+
"",
|
|
1969
|
+
`Adoption — ${adoption.level}. ${adoption.summary}`,
|
|
1970
|
+
` ${adoption.recommendation}`,
|
|
1971
|
+
...swapLines(adoption, MAX_DETAILS),
|
|
1972
|
+
"",
|
|
1973
|
+
].join("\n")
|
|
1974
|
+
}
|
|
1843
1975
|
const manualText = () =>
|
|
1844
1976
|
[
|
|
1845
1977
|
"",
|
|
@@ -1869,7 +2001,7 @@ function cmdCheck() {
|
|
|
1869
2001
|
}
|
|
1870
2002
|
|
|
1871
2003
|
if (JSON_OUT) process.stdout.write(`${JSON.stringify({ type: "check", data }, null, DENSE ? 0 : 1)}\n`)
|
|
1872
|
-
else process.stdout.write(`${summaryText()}\n${inventoryText()}${manualText()}\n`)
|
|
2004
|
+
else process.stdout.write(`${summaryText()}\n${adoptionText()}${inventoryText()}${manualText()}\n`)
|
|
1873
2005
|
logInvocation(failed.length ? 1 : 0, failed.length ? "ERR_CHECK_FAILED" : undefined)
|
|
1874
2006
|
process.exit(failed.length ? 1 : 0)
|
|
1875
2007
|
}
|
|
@@ -1899,6 +2031,15 @@ function cmdDesignTool(script) {
|
|
|
1899
2031
|
passThrough(runScript(join(PKG_ROOT, "scripts", script)))
|
|
1900
2032
|
}
|
|
1901
2033
|
|
|
2034
|
+
/** What setup did to the project instructions, in one line. */
|
|
2035
|
+
function instructionsText(data) {
|
|
2036
|
+
if (!data.instructions.length) return "Project instructions: skipped (--no-instructions). Agents use the skill only when a request names it."
|
|
2037
|
+
const changed = data.instructionsChanged
|
|
2038
|
+
const files = data.instructions.join(", ")
|
|
2039
|
+
if (!changed.length) return `Project instructions: ${files} already carry the Sable block.`
|
|
2040
|
+
return `Project instructions: ${data.dryRun ? "would add" : "added"} the Sable block to ${changed.join(", ")}, so every UI request goes through the skill. Edit outside its markers; --no-instructions skips it.`
|
|
2041
|
+
}
|
|
2042
|
+
|
|
1902
2043
|
function cmdSetup() {
|
|
1903
2044
|
if (!flags.has("agent") || rest.length) fail("ERR_BAD_ARGUMENT", "Use sable setup --agent from your project root.", ["sable setup --agent --dry-run"])
|
|
1904
2045
|
const root = flags.get("root")
|
|
@@ -1908,7 +2049,7 @@ function cmdSetup() {
|
|
|
1908
2049
|
if (flags.has("hook") && !forClaude)
|
|
1909
2050
|
fail("ERR_BAD_ARGUMENT", "--hook installs a Claude Code Stop hook; use it with --target claude or all.", ["sable setup --agent --target claude --hook"])
|
|
1910
2051
|
try {
|
|
1911
|
-
const options = { root: root ? resolve(CWD, root) : CWD, project: CWD, packageRoot: PKG_ROOT, target, update: flags.has("update"), dryRun: flags.has("dry-run") }
|
|
2052
|
+
const options = { root: root ? resolve(CWD, root) : CWD, project: CWD, packageRoot: PKG_ROOT, target, update: flags.has("update"), dryRun: flags.has("dry-run"), instructions: !flags.has("no-instructions") }
|
|
1912
2053
|
// Preflight the settings file too, so a refusal there writes nothing anywhere.
|
|
1913
2054
|
if (flags.has("hook")) installClaudeStopHook({ ...options, dryRun: true })
|
|
1914
2055
|
const data = installAgentSkill(options)
|
|
@@ -1922,7 +2063,7 @@ function cmdSetup() {
|
|
|
1922
2063
|
: hook.installed
|
|
1923
2064
|
? `\n${data.dryRun ? "Would add" : hook.alreadyPresent ? "Already present:" : "Added"} a Claude Code Stop hook to ${hook.settingsFile}: Claude cannot end a task while \`sable check\` fails.`
|
|
1924
2065
|
: `\nOptional: block Claude Code from ending a task while \`sable check\` fails. Add this to ${hook.settingsFile} (merge into an existing "hooks" key), or rerun with --hook to add it:\n\n${JSON.stringify(hook.settings, null, 2)}\n\nThe hook runs: ${hook.command}`
|
|
1925
|
-
ok("setup.agent", data, `${data.dryRun ? "Would install" : "Installed"} Sable skill entries at ${data.destinations.map((d) => relative(CWD, join(data.root, d)).replaceAll("\\", "/")).join(", ")}.\nNext: ${data.next}${hookText}`)
|
|
2066
|
+
ok("setup.agent", data, `${data.dryRun ? "Would install" : "Installed"} Sable skill entries at ${data.destinations.map((d) => relative(CWD, join(data.root, d)).replaceAll("\\", "/")).join(", ")}.\n${instructionsText(data)}\nNext: ${data.next}${hookText}`)
|
|
1926
2067
|
} catch (error) {
|
|
1927
2068
|
fail("ERR_AGENT_SETUP", error.message, ["sable setup --agent --dry-run"])
|
|
1928
2069
|
}
|
|
@@ -1961,6 +2102,7 @@ const HANDLERS = {
|
|
|
1961
2102
|
docs: cmdDocs,
|
|
1962
2103
|
doctor: cmdDoctor,
|
|
1963
2104
|
init: cmdInit,
|
|
2105
|
+
upgrade: cmdUpgrade,
|
|
1964
2106
|
grade: cmdGrade,
|
|
1965
2107
|
learn: cmdLearn,
|
|
1966
2108
|
atlas: () => cmdDesignTool("design-atlas.mjs"),
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** One day of activity. Only `date` and `value` are required. */
|
|
2
|
+
interface ActivityDay {
|
|
3
|
+
/** The calendar day, "YYYY-MM-DD". Read as a day, never as an instant. */
|
|
4
|
+
date: string;
|
|
5
|
+
/** The measure for that day. A day missing from the data counts as 0. */
|
|
6
|
+
value: number;
|
|
7
|
+
/** 0…levels-1. Overrides the computed level — GitHub's API sends its own. */
|
|
8
|
+
level?: number;
|
|
9
|
+
/** A key into the heatmap's `categories`, for multi-source cells. */
|
|
10
|
+
category?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Days as a list, or as a map of date to value (GitLab's and Mantine's shape). */
|
|
13
|
+
type ActivityData = ActivityDay[] | Record<string, number>;
|
|
14
|
+
/**
|
|
15
|
+
* Several sources' days merged into one list with categories: a day active in
|
|
16
|
+
* one source takes that source's key, a day active in more than one takes
|
|
17
|
+
* `both`. Values sum. For "GitHub, Codex, or both on that day".
|
|
18
|
+
*
|
|
19
|
+
* @example mergeActivity({ github: githubDays, codex: codexDays })
|
|
20
|
+
*/
|
|
21
|
+
declare function mergeActivity(sources: Record<string, ActivityData>, { both }?: {
|
|
22
|
+
both?: string;
|
|
23
|
+
}): ActivityDay[];
|
|
24
|
+
|
|
25
|
+
export { type ActivityDay as A, type ActivityData as a, mergeActivity as m };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { A as ActivityDay } from '../activity-DT8VK9fx.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GitHub contribution data → ActivityHeatmap days. Pure transforms: Sable never
|
|
5
|
+
* fetches. GitHub's GraphQL API needs a token, so call it from your server and
|
|
6
|
+
* pass the result through; the docs page shows a route handler that does.
|
|
7
|
+
*/
|
|
8
|
+
/** The `contributionLevel` enum GitHub's GraphQL API returns per day. */
|
|
9
|
+
type GitHubContributionLevel = "NONE" | "FIRST_QUARTILE" | "SECOND_QUARTILE" | "THIRD_QUARTILE" | "FOURTH_QUARTILE";
|
|
10
|
+
/** The slice of `user.contributionsCollection.contributionCalendar` the transform reads. */
|
|
11
|
+
interface GitHubContributionCalendar {
|
|
12
|
+
weeks: {
|
|
13
|
+
contributionDays: {
|
|
14
|
+
date: string;
|
|
15
|
+
contributionCount: number;
|
|
16
|
+
contributionLevel?: GitHubContributionLevel;
|
|
17
|
+
}[];
|
|
18
|
+
}[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* From GraphQL `contributionsCollection { contributionCalendar { weeks { contributionDays
|
|
22
|
+
* { date contributionCount contributionLevel } } } }`. GitHub's own quartile levels pass
|
|
23
|
+
* through as 0–4, so the grid matches the profile page exactly (use the default 5 levels).
|
|
24
|
+
*/
|
|
25
|
+
declare function fromGitHubGraphQL(calendar: GitHubContributionCalendar): ActivityDay[];
|
|
26
|
+
/** The response of the community github-contributions-api (`/v4/{user}`). */
|
|
27
|
+
interface GitHubContributionsApiResponse {
|
|
28
|
+
contributions: {
|
|
29
|
+
date: string;
|
|
30
|
+
count: number;
|
|
31
|
+
level?: number;
|
|
32
|
+
}[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* From the community github-contributions-api (`https://github-contributions-api.jogruber.de/v4/{user}`).
|
|
36
|
+
* It is a third-party scraper of the profile page — cached for an hour and rate
|
|
37
|
+
* limited — so treat it as a convenience for demos, not a dependency.
|
|
38
|
+
*/
|
|
39
|
+
declare function fromGitHubContributionsApi(json: GitHubContributionsApiResponse): ActivityDay[];
|
|
40
|
+
|
|
41
|
+
export { type GitHubContributionCalendar, type GitHubContributionLevel, type GitHubContributionsApiResponse, fromGitHubContributionsApi, fromGitHubGraphQL };
|