@estiva-app/ui 0.21.0 → 0.22.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/dist/Avatar.d.ts +3 -0
- package/dist/Avatar.d.ts.map +1 -1
- package/dist/ChipInput.d.ts +3 -0
- package/dist/ChipInput.d.ts.map +1 -1
- package/dist/CommandPalette.d.ts +6 -0
- package/dist/CommandPalette.d.ts.map +1 -1
- package/dist/IdentityMenu.d.ts +3 -0
- package/dist/IdentityMenu.d.ts.map +1 -1
- package/dist/Menu.d.ts +7 -0
- package/dist/Menu.d.ts.map +1 -1
- package/dist/Skeleton.d.ts +2 -0
- package/dist/Skeleton.d.ts.map +1 -1
- package/dist/Toast.d.ts +5 -0
- package/dist/Toast.d.ts.map +1 -1
- package/dist/Toolbar.d.ts +4 -0
- package/dist/Toolbar.d.ts.map +1 -1
- package/dist/Tooltip.d.ts +4 -0
- package/dist/Tooltip.d.ts.map +1 -1
- package/dist/gates/app-checks.d.ts.map +1 -1
- package/dist/gates/{chunk-AWO7SZSA.js → chunk-AUXD4GCY.js} +10 -9
- package/dist/gates/chunk-AUXD4GCY.js.map +7 -0
- package/dist/gates/cli.js +2 -2
- package/dist/gates/cli.js.map +2 -2
- package/dist/gates/index.js +4 -2
- package/dist/gates/index.js.map +2 -2
- package/dist/gates/status.d.ts +5 -1
- package/dist/gates/status.d.ts.map +1 -1
- package/dist/index.js.map +2 -2
- package/dist/registry/build-GOVLABI6.js +13 -0
- package/dist/registry/build-GOVLABI6.js.map +7 -0
- package/dist/registry/build.d.ts +29 -0
- package/dist/registry/build.d.ts.map +1 -0
- package/dist/registry/chunk-IJNCYVH4.js +163 -0
- package/dist/registry/chunk-IJNCYVH4.js.map +7 -0
- package/dist/registry/chunk-MRSBS5OP.js +99 -0
- package/dist/registry/chunk-MRSBS5OP.js.map +7 -0
- package/dist/registry/chunk-QDYGB3QN.js +352 -0
- package/dist/registry/chunk-QDYGB3QN.js.map +7 -0
- package/dist/registry/cli.d.ts +2 -0
- package/dist/registry/cli.d.ts.map +1 -0
- package/dist/registry/cli.js +121 -0
- package/dist/registry/cli.js.map +7 -0
- package/dist/registry/find.d.ts +43 -0
- package/dist/registry/find.d.ts.map +1 -0
- package/dist/registry/index.d.ts +14 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +26 -0
- package/dist/registry/index.js.map +7 -0
- package/dist/registry/schema.d.ts +147 -0
- package/dist/registry/schema.d.ts.map +1 -0
- package/package.json +15 -2
- package/registry.json +4470 -0
- package/src/Avatar.tsx +3 -0
- package/src/ChipInput.tsx +3 -0
- package/src/CommandPalette.tsx +6 -0
- package/src/IdentityMenu.tsx +3 -0
- package/src/Menu.tsx +7 -0
- package/src/Skeleton.tsx +2 -0
- package/src/Toast.tsx +5 -0
- package/src/Toolbar.tsx +4 -0
- package/src/Tooltip.tsx +4 -0
- package/src/gates/app-checks.ts +3 -1
- package/src/gates/cli.ts +8 -2
- package/src/gates/gates.test.ts +11 -0
- package/src/gates/status.ts +24 -11
- package/src/registry/build.ts +590 -0
- package/src/registry/cli.ts +141 -0
- package/src/registry/find.ts +195 -0
- package/src/registry/index.ts +24 -0
- package/src/registry/registry.test.ts +448 -0
- package/src/registry/schema.ts +268 -0
- package/dist/gates/chunk-AWO7SZSA.js.map +0 -7
package/src/Avatar.tsx
CHANGED
|
@@ -29,6 +29,9 @@ import { cn } from './cn'
|
|
|
29
29
|
*/
|
|
30
30
|
const HUES = ['#56c8ff', '#ff8f6b', '#4ade8c', '#b18cff', '#ffc94d', '#ff7eb0', '#7ea8ff', '#5fdfd6']
|
|
31
31
|
|
|
32
|
+
/** The colour a person's initials sit on, chosen from a key: the same key
|
|
33
|
+
* always gives the same hue, so a face reads as theirs in every app. The
|
|
34
|
+
* eight are a fallback palette, not tokens — see the note above `HUES`. */
|
|
32
35
|
export const hueFor = (key: string) => {
|
|
33
36
|
let h = 0
|
|
34
37
|
for (let i = 0; i < key.length; i++) h = (h * 31 + key.charCodeAt(i)) | 0
|
package/src/ChipInput.tsx
CHANGED
|
@@ -54,6 +54,9 @@ export interface InputChipProps {
|
|
|
54
54
|
className?: string
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/** A chip standing on its own: a label, an optional leading icon, an optional
|
|
58
|
+
* ✕. For a caller that keeps its own list. Inside a `ChipInput` the same look
|
|
59
|
+
* goes onto Base UI's `Combobox.Chip`, which joins the field's keyboard. */
|
|
57
60
|
export function InputChip({ label, leading, onRemove, removeLabel, truncate, className }: InputChipProps) {
|
|
58
61
|
return (
|
|
59
62
|
<div className={cn(CHIP_BOX, chipPadding(!!leading, !!onRemove), className)}>
|
package/src/CommandPalette.tsx
CHANGED
|
@@ -237,6 +237,9 @@ export interface CommandPaletteSearchProps {
|
|
|
237
237
|
|
|
238
238
|
type ListGroup = { value: string; items: CommandPaletteRow[] }
|
|
239
239
|
|
|
240
|
+
/** The palette as a search: a query field, rows in labelled groups walked with
|
|
241
|
+
* the arrow keys, and what to show while nothing has arrived and when nothing
|
|
242
|
+
* matches. */
|
|
240
243
|
export function CommandPaletteSearch({ query, onQueryChange, placeholder, groups, chip, pending, notes = [], empty, children }: CommandPaletteSearchProps) {
|
|
241
244
|
const { modKey, popupRef } = usePalette('CommandPaletteSearch')
|
|
242
245
|
const back = useBack(chip, popupRef)
|
|
@@ -478,6 +481,9 @@ function isTextField(el: Element | null): el is HTMLInputElement | HTMLTextAreaE
|
|
|
478
481
|
return el instanceof HTMLInputElement && TEXT_TYPES.has(el.getAttribute('type') ?? '') && el.getAttribute('role') !== 'combobox' && el.tabIndex >= 0
|
|
479
482
|
}
|
|
480
483
|
|
|
484
|
+
/** The palette as a form: fields, one submit button, and Ctrl+Enter — both ways
|
|
485
|
+
* in run Base UI's field check. It shows what it is working on and what failed,
|
|
486
|
+
* and sends focus to the first field that needs something. */
|
|
481
487
|
export function CommandPaletteForm({ chip, icon, submitLabel, onSubmit, submitWaits, working, error, children }: CommandPaletteFormProps) {
|
|
482
488
|
const { modKey, popupRef } = usePalette('CommandPaletteForm')
|
|
483
489
|
const back = useBack(chip, popupRef)
|
package/src/IdentityMenu.tsx
CHANGED
|
@@ -62,6 +62,9 @@ export interface IdentityPanelProps extends Omit<IdentityMenuProps, 'compact'> {
|
|
|
62
62
|
onClose?: () => void
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** The identity rows on a panel of their own, with no menu around them — for a
|
|
66
|
+
* docs page, or anywhere the panel is already placed. `IdentityMenu` is the
|
|
67
|
+
* same rows inside a real menu. */
|
|
65
68
|
export function IdentityPanel({ className, onClose = () => {}, ...rest }: IdentityPanelProps) {
|
|
66
69
|
return (
|
|
67
70
|
<MenuPanel className={cn('w-72', className)}>
|
package/src/Menu.tsx
CHANGED
|
@@ -86,6 +86,10 @@ export interface MenuPanelProps extends Omit<ComponentPropsWithRef<'div'>, 'chil
|
|
|
86
86
|
children?: ReactNode
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/** The menu's surface without the menu: the elevated box, the rows' padding,
|
|
90
|
+
* and a divider that runs the full width of what it separates. `Menu` renders
|
|
91
|
+
* its popup as one. On its own it is for the places that draw a menu's look
|
|
92
|
+
* but not its behaviour — a docs page, Peek's `@`, `/` and `[` pickers. */
|
|
89
93
|
export function MenuPanel({ children, className, ...props }: MenuPanelProps) {
|
|
90
94
|
return (
|
|
91
95
|
<div
|
|
@@ -277,6 +281,9 @@ export interface MenuSubProps {
|
|
|
277
281
|
className?: string
|
|
278
282
|
}
|
|
279
283
|
|
|
284
|
+
/** A row that opens another menu beside it; its rows are this row's children.
|
|
285
|
+
* Never inside a menu opened on hover — leaving the submenu strands both open,
|
|
286
|
+
* measured below. */
|
|
280
287
|
export function MenuSub({ label, leading, selected, children, className }: MenuSubProps) {
|
|
281
288
|
/*
|
|
282
289
|
* A submenu inside a hover-opened menu strands it.
|
package/src/Skeleton.tsx
CHANGED
|
@@ -10,6 +10,8 @@ import { cn } from './cn'
|
|
|
10
10
|
* A list reveals after 150ms (`animate-skeleton-in`, in the preset) so a
|
|
11
11
|
* fast load never flashes a skeleton — Peek's rule.
|
|
12
12
|
*/
|
|
13
|
+
/** One pulsing bar at the inset colour: the piece every placeholder here is
|
|
14
|
+
* built from. It draws nothing of its own — give it a width and a height. */
|
|
13
15
|
export function SkeletonBar({ className, style }: { className?: string; style?: CSSProperties }) {
|
|
14
16
|
return <div className={cn('bg-bg-inset rounded-sm animate-pulse', className)} style={style} />
|
|
15
17
|
}
|
package/src/Toast.tsx
CHANGED
|
@@ -153,6 +153,9 @@ const ToastContext = createContext<ToastValue | null>(null)
|
|
|
153
153
|
/** D7: three on screen at once. */
|
|
154
154
|
const VISIBLE_TOASTS = 3
|
|
155
155
|
|
|
156
|
+
/** Mount once, near the top of an app. It owns the stack, the timers and the
|
|
157
|
+
* portal every toast goes through, and it is what `useToast` talks to. Three
|
|
158
|
+
* show at once; a fourth waits for one of them to close. */
|
|
156
159
|
export function ToastProvider({ children }: { children: ReactNode }) {
|
|
157
160
|
// One manager per provider, so `useToast` can add and close without
|
|
158
161
|
// re-rendering every caller each time the list changes.
|
|
@@ -227,6 +230,8 @@ function ToastList() {
|
|
|
227
230
|
})
|
|
228
231
|
}
|
|
229
232
|
|
|
233
|
+
/** How anything under a `ToastProvider` raises a toast (`showToast`) or takes
|
|
234
|
+
* one away (`dismissToast`). Throws outside a provider. */
|
|
230
235
|
export function useToast(): ToastValue {
|
|
231
236
|
const ctx = useContext(ToastContext)
|
|
232
237
|
if (!ctx) throw new Error('useToast must be used within ToastProvider')
|
package/src/Toolbar.tsx
CHANGED
|
@@ -104,6 +104,8 @@ export interface ToolbarButtonProps extends IconButtonProps {
|
|
|
104
104
|
ref?: Ref<HTMLButtonElement>
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/** An `IconButton` that is an item of a `Toolbar`: the arrow keys reach it, and
|
|
108
|
+
* a disabled one stays in the walk so its reason can still be read. */
|
|
107
109
|
export function ToolbarButton({ ref, disabled, disabledReason, ...props }: ToolbarButtonProps) {
|
|
108
110
|
return (
|
|
109
111
|
<BaseToolbar.Button
|
|
@@ -137,6 +139,8 @@ export function ToolbarButton({ ref, disabled, disabledReason, ...props }: Toolb
|
|
|
137
139
|
*/
|
|
138
140
|
export type ToolbarInputProps = TextInputProps
|
|
139
141
|
|
|
142
|
+
/** A `TextInput` that is an item of a `Toolbar`, so the arrow keys walk into the
|
|
143
|
+
* field and out of it again. */
|
|
140
144
|
export function ToolbarInput({ size, ...props }: ToolbarInputProps) {
|
|
141
145
|
// `size` is TextInput's own (default or small), not the native attribute
|
|
142
146
|
// Base UI's part would take, so it goes to the field it draws.
|
package/src/Tooltip.tsx
CHANGED
|
@@ -146,6 +146,10 @@ function TooltipSurface({ label, shortcut, placement }: { label: string; shortcu
|
|
|
146
146
|
)
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/** A tooltip on something that cannot be its own trigger — a span, a `Chip`,
|
|
150
|
+
* an icon. It wraps what it is given in an element that carries the handlers.
|
|
151
|
+
* A control that can be the trigger does it itself: `IconButton` takes a
|
|
152
|
+
* `tooltip` prop, and `Button` shows a `disabledReason` the same way. */
|
|
149
153
|
export function WithTooltip({ label, shortcut, placement = 'top', wrapperClassName, children }: WithTooltipProps) {
|
|
150
154
|
return (
|
|
151
155
|
<BaseTooltip.Root disableHoverablePopup>
|
package/src/gates/app-checks.ts
CHANGED
|
@@ -216,7 +216,9 @@ export function appChecks(h: GateHelpers, { app = '.', page, chain = { ref: 'UIG
|
|
|
216
216
|
{ what: 'the gate pieces come from the package, and no repo keeps a copy', run: all(h, [
|
|
217
217
|
() => h.contains(at('eslint.gates.config.js'), /from '@estiva-app\/ui\/gates'/, 'the gate config imports @estiva-app/ui/gates'),
|
|
218
218
|
() => h.hook('.claude/settings.json', '@estiva-app/ui/dist/gates/cli.js'),
|
|
219
|
-
|
|
219
|
+
// `estiva-gates status` where npm can find the command; by path where it cannot,
|
|
220
|
+
// because the install is in the app's folder and the script is in the repo's top one (Ship).
|
|
221
|
+
() => h.contains('package.json', /"gates:status":\s*"[^"]*(?:estiva-gates|@estiva-app\/ui\/dist\/gates\/cli\.js)[^"]*status/, "gates:status runs the package's engine"),
|
|
220
222
|
() => (h.exists('scripts/gates-status.mjs') ? h.FAIL('scripts/gates-status.mjs is a copy of the status engine: run estiva-gates status instead') : h.PASS('no copy of the status engine')),
|
|
221
223
|
], 'the gate config, the hook and gates:status are the package\'s; no copy of the engine') },
|
|
222
224
|
]),
|
package/src/gates/cli.ts
CHANGED
|
@@ -4,13 +4,19 @@
|
|
|
4
4
|
*
|
|
5
5
|
* estiva-gates count --repo <name> [--package] write .gates-count.json (postlint:rules)
|
|
6
6
|
* estiva-gates hook [--app <dir>] [--package] the editor gate, run by a PreToolUse hook
|
|
7
|
-
* estiva-gates status [--json] [--detail]
|
|
7
|
+
* estiva-gates status [--app <dir>] [--json] [--detail] gates:status
|
|
8
8
|
*
|
|
9
9
|
* An app's `package.json` runs the first and the last through npm, which finds
|
|
10
10
|
* the command in `node_modules/.bin`. The hook is run by path, from the
|
|
11
11
|
* committed `.claude/settings.json`:
|
|
12
12
|
*
|
|
13
13
|
* node "$CLAUDE_PROJECT_DIR/node_modules/@estiva-app/ui/dist/gates/cli.js" hook
|
|
14
|
+
*
|
|
15
|
+
* A repo whose app sits in a folder of its own runs both by path, from the
|
|
16
|
+
* repo's top folder, and says where the app is — Ship (UIG-32):
|
|
17
|
+
*
|
|
18
|
+
* node "$CLAUDE_PROJECT_DIR/web/node_modules/@estiva-app/ui/dist/gates/cli.js" hook --app web
|
|
19
|
+
* node web/node_modules/@estiva-app/ui/dist/gates/cli.js status --app web
|
|
14
20
|
*/
|
|
15
21
|
import { writeGateCount } from './count'
|
|
16
22
|
import { runHook } from './hook'
|
|
@@ -43,7 +49,7 @@ async function main(): Promise<number> {
|
|
|
43
49
|
return result.code
|
|
44
50
|
}
|
|
45
51
|
case 'status': {
|
|
46
|
-
process.stdout.write(`${await runStatus({ json: flag('json'), detail: flag('detail') })}${flag('json') ? '' : '\n'}`)
|
|
52
|
+
process.stdout.write(`${await runStatus({ app: value('app'), json: flag('json'), detail: flag('detail') })}${flag('json') ? '' : '\n'}`)
|
|
47
53
|
return 0
|
|
48
54
|
}
|
|
49
55
|
default:
|
package/src/gates/gates.test.ts
CHANGED
|
@@ -180,4 +180,15 @@ describe('gates:status', () => {
|
|
|
180
180
|
const json = JSON.parse(await runStatus({ root: dir, json: true }))
|
|
181
181
|
expect(json.engine).toMatch(/^@estiva-app\/ui@\d+\.\d+\.\d+$/)
|
|
182
182
|
})
|
|
183
|
+
|
|
184
|
+
it('finds the checks file beside the app, and still reads the repo from its top (Ship, UIG-32)', async () => {
|
|
185
|
+
// The checks file sits in `web/`, where the install is; what it names is read from the top folder.
|
|
186
|
+
const top = join(scratch, 'status-web')
|
|
187
|
+
mkdirSync(join(top, 'web', 'scripts'), { recursive: true })
|
|
188
|
+
writeFileSync(join(top, 'package.json'), JSON.stringify({ name: 'status-web', scripts: { 'gates:status': 'node web/node_modules/@estiva-app/ui/dist/gates/cli.js status --app web' } }))
|
|
189
|
+
writeFileSync(join(top, 'web', 'scripts', 'gates-checks.mjs'), "export default (h) => ({ repo: 'shipish', tickets: [\n { ref: 'UIG-4', title: 'The chain', owner: true, checks: [{ what: 'the top folder is read', run: () => h.script('package.json', 'gates:status') }] },\n] })\n")
|
|
190
|
+
const out = await runStatus({ root: top, app: 'web' })
|
|
191
|
+
expect(out).toContain('Tickets shipish owns (1):')
|
|
192
|
+
expect(out).toContain('✅ UIG-4')
|
|
193
|
+
})
|
|
183
194
|
})
|
package/src/gates/status.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* estiva-gates status one row per ticket
|
|
6
6
|
* estiva-gates status --detail every check under every row
|
|
7
7
|
* estiva-gates status --json machine output, read by estiva-ui's run
|
|
8
|
+
* estiva-gates status --app web the app sits in `web/`, as Ship's does
|
|
8
9
|
*
|
|
9
10
|
* Every row is decided by checks on real files, a real lint run or a real
|
|
10
11
|
* GitHub setting. Nothing here reads a list that someone ticks by hand.
|
|
@@ -16,8 +17,10 @@
|
|
|
16
17
|
* copy is named as one when estiva-ui runs it; UIG-32 moves Peek and Ship off
|
|
17
18
|
* theirs.
|
|
18
19
|
*
|
|
19
|
-
* What differs per repo is `scripts/gates-checks.mjs`,
|
|
20
|
-
* or the checks every app runs (`appChecks`) plus its own.
|
|
20
|
+
* What differs per repo is `scripts/gates-checks.mjs`, beside the app: its
|
|
21
|
+
* checks, or the checks every app runs (`appChecks`) plus its own. It sits with
|
|
22
|
+
* the app because it imports the package — `web/scripts/` in Ship, whose app,
|
|
23
|
+
* and whose install, are in `web/` (UIG-32).
|
|
21
24
|
*
|
|
22
25
|
* The rows and the reasons for each check are in estiva-ui docs/GATES.md §15
|
|
23
26
|
* and §17.
|
|
@@ -93,7 +96,8 @@ export interface GateSpec {
|
|
|
93
96
|
tickets: GateTicket[]
|
|
94
97
|
/** estiva-ui only: every ticket, and the repos it joins in. */
|
|
95
98
|
all?: TicketListEntry[]
|
|
96
|
-
|
|
99
|
+
/** estiva-ui only: the repos beside it. `app` is the folder its app sits in: `web` in Ship. */
|
|
100
|
+
siblings?: { name: string; path: string; env: string; app?: string }[]
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
interface Row {
|
|
@@ -356,8 +360,11 @@ function statusOf(checks: CheckResult[]) {
|
|
|
356
360
|
return fail > 0 ? 'none' : 'unknown'
|
|
357
361
|
}
|
|
358
362
|
|
|
359
|
-
async function runRepo(ROOT: string, h: GateHelpers): Promise<{ spec: GateSpec; report: Report }> {
|
|
360
|
-
|
|
363
|
+
async function runRepo(ROOT: string, app: string, h: GateHelpers): Promise<{ spec: GateSpec; report: Report }> {
|
|
364
|
+
// The checks file sits with the app, because it imports `appChecks` from the
|
|
365
|
+
// package: in Ship that install is `web/node_modules`, not the repo's top
|
|
366
|
+
// folder (UIG-32). Everything it names is still read from the repo's top.
|
|
367
|
+
const checksFile = join(ROOT, app, 'scripts', 'gates-checks.mjs')
|
|
361
368
|
const define = ((await import(pathToFileURL(checksFile).href)) as { default: (h: GateHelpers) => GateSpec }).default
|
|
362
369
|
const spec = define(h)
|
|
363
370
|
const git = (...a: string[]) => {
|
|
@@ -388,18 +395,22 @@ async function runRepo(ROOT: string, h: GateHelpers): Promise<{ spec: GateSpec;
|
|
|
388
395
|
/**
|
|
389
396
|
* Another repository's report. A repo on the package's engine runs it from its
|
|
390
397
|
* own install; a repo still carrying `scripts/gates-status.mjs` runs that copy.
|
|
398
|
+
*
|
|
399
|
+
* `app` is the folder that repo's app sits in — `web` in Ship — where its
|
|
400
|
+
* install and its checks file are. The report is still of the whole repo.
|
|
391
401
|
*/
|
|
392
|
-
function runSibling(dir: string, shown: string): { report?: Report; error?: string } {
|
|
402
|
+
function runSibling(dir: string, app: string, shown: string): { report?: Report; error?: string } {
|
|
393
403
|
const copy = join(dir, 'scripts', 'gates-status.mjs')
|
|
394
|
-
const installed = createRequire(join(dir, 'package.json'))
|
|
404
|
+
const installed = createRequire(join(dir, app, 'package.json'))
|
|
395
405
|
let script = copy
|
|
396
406
|
const args = ['--json']
|
|
397
407
|
if (!existsSync(copy)) {
|
|
398
408
|
try {
|
|
399
409
|
script = join(installed.resolve('@estiva-app/ui/package.json'), '..', 'dist', 'gates', 'cli.js')
|
|
400
410
|
args.unshift('status')
|
|
411
|
+
if (app !== '.') args.push('--app', app)
|
|
401
412
|
} catch {
|
|
402
|
-
return { error: `${shown} has neither its own gates-status.mjs nor @estiva-app/ui installed` }
|
|
413
|
+
return { error: `${shown} has neither its own gates-status.mjs nor @estiva-app/ui installed${app === '.' ? '' : ` in ${app}`}` }
|
|
403
414
|
}
|
|
404
415
|
if (!existsSync(script)) return { error: `${shown} has an @estiva-app/ui with no gates engine: install 0.21.0 or later` }
|
|
405
416
|
}
|
|
@@ -442,16 +453,18 @@ function summary(rows: Row[]) {
|
|
|
442
453
|
export interface StatusOptions {
|
|
443
454
|
/** The repository to report on. */
|
|
444
455
|
root?: string
|
|
456
|
+
/** The folder that holds the app, relative to `root`: `web` in Ship, `.` elsewhere. */
|
|
457
|
+
app?: string
|
|
445
458
|
json?: boolean
|
|
446
459
|
detail?: boolean
|
|
447
460
|
}
|
|
448
461
|
|
|
449
462
|
/** Print where the project stands. Returns what was printed, or the JSON report. */
|
|
450
|
-
export async function runStatus({ root = process.cwd(), json = false, detail = false }: StatusOptions = {}): Promise<string> {
|
|
463
|
+
export async function runStatus({ root = process.cwd(), app = '.', json = false, detail = false }: StatusOptions = {}): Promise<string> {
|
|
451
464
|
const ROOT = resolve(root)
|
|
452
465
|
const h = helpers(ROOT)
|
|
453
466
|
const shown = (dir: string) => (relative(ROOT, dir) || dir).replace(/\\/g, '/')
|
|
454
|
-
const { spec, report } = await runRepo(ROOT, h)
|
|
467
|
+
const { spec, report } = await runRepo(ROOT, app, h)
|
|
455
468
|
|
|
456
469
|
if (json) return JSON.stringify(report)
|
|
457
470
|
|
|
@@ -478,7 +491,7 @@ export async function runStatus({ root = process.cwd(), json = false, detail = f
|
|
|
478
491
|
found.push({ name: s.name, note: `not found at ${shown(dir)} (set ${s.env} to point at it)`, missing: true })
|
|
479
492
|
continue
|
|
480
493
|
}
|
|
481
|
-
const r = runSibling(dir, shown(dir))
|
|
494
|
+
const r = runSibling(dir, s.app ?? '.', shown(dir))
|
|
482
495
|
if (!r.report) {
|
|
483
496
|
found.push({ name: s.name, note: r.error ?? 'no report', missing: true })
|
|
484
497
|
continue
|