agent-facets 0.2.2 → 0.3.3
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/bin/facet +181 -0
- package/bin/package.json +3 -0
- package/package.json +17 -37
- package/postinstall.mjs +210 -0
- package/.package.json.bak +0 -44
- package/.turbo/turbo-build.log +0 -3
- package/CHANGELOG.md +0 -85
- package/bunfig.toml +0 -2
- package/dist/facet +0 -0
- package/src/__tests__/cli.test.ts +0 -195
- package/src/__tests__/create-build.test.ts +0 -227
- package/src/__tests__/edit-integration.test.ts +0 -171
- package/src/__tests__/resolve-dir.test.ts +0 -95
- package/src/commands/build.ts +0 -58
- package/src/commands/create/index.ts +0 -76
- package/src/commands/create/types.ts +0 -9
- package/src/commands/create/wizard.tsx +0 -75
- package/src/commands/create-scaffold.ts +0 -184
- package/src/commands/edit/index.ts +0 -144
- package/src/commands/edit/wizard.tsx +0 -74
- package/src/commands/resolve-dir.ts +0 -98
- package/src/commands.ts +0 -40
- package/src/help.ts +0 -43
- package/src/index.ts +0 -10
- package/src/run.ts +0 -82
- package/src/suggest.ts +0 -35
- package/src/tui/components/asset-description.tsx +0 -17
- package/src/tui/components/asset-field-picker.tsx +0 -78
- package/src/tui/components/asset-inline-input.tsx +0 -91
- package/src/tui/components/asset-item.tsx +0 -44
- package/src/tui/components/asset-section.tsx +0 -191
- package/src/tui/components/button.tsx +0 -92
- package/src/tui/components/editable-field.tsx +0 -172
- package/src/tui/components/exit-toast.tsx +0 -20
- package/src/tui/components/reconciliation-item.tsx +0 -129
- package/src/tui/components/stage-row.tsx +0 -45
- package/src/tui/components/version-selector.tsx +0 -79
- package/src/tui/context/focus-mode-context.ts +0 -36
- package/src/tui/context/focus-order-context.ts +0 -68
- package/src/tui/context/form-state-context.ts +0 -260
- package/src/tui/editor.ts +0 -40
- package/src/tui/gradient.ts +0 -1
- package/src/tui/hooks/use-exit-keys.ts +0 -75
- package/src/tui/hooks/use-navigation-keys.ts +0 -34
- package/src/tui/layouts/wizard-layout.tsx +0 -41
- package/src/tui/theme.ts +0 -1
- package/src/tui/views/build/build-view.tsx +0 -152
- package/src/tui/views/create/confirm-view.tsx +0 -74
- package/src/tui/views/create/create-view.tsx +0 -158
- package/src/tui/views/create/wizard.tsx +0 -97
- package/src/tui/views/edit/edit-confirm-view.tsx +0 -93
- package/src/tui/views/edit/edit-types.ts +0 -34
- package/src/tui/views/edit/edit-view.tsx +0 -140
- package/src/tui/views/edit/manifest-to-form.ts +0 -38
- package/src/tui/views/edit/reconciliation-view.tsx +0 -170
- package/src/tui/views/edit/use-edit-session.ts +0 -125
- package/src/tui/views/edit/wizard.tsx +0 -129
- package/src/version.ts +0 -3
- package/tsconfig.json +0 -4
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { mkdir, stat } from 'node:fs/promises'
|
|
2
|
-
import { dirname, join, resolve } from 'node:path'
|
|
3
|
-
import { FACET_MANIFEST_FILE } from '@agent-facets/core'
|
|
4
|
-
|
|
5
|
-
export interface ResolvedDir {
|
|
6
|
-
ok: true
|
|
7
|
-
dir: string
|
|
8
|
-
display: string
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface ResolvedDirError {
|
|
12
|
-
ok: false
|
|
13
|
-
message: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type ResolveResult = ResolvedDir | ResolvedDirError
|
|
17
|
-
|
|
18
|
-
export interface ResolveOptions {
|
|
19
|
-
mustExist: boolean
|
|
20
|
-
facetMustExist?: boolean
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Validates and resolves a directory argument for CLI commands.
|
|
25
|
-
*
|
|
26
|
-
* Handles:
|
|
27
|
-
* - No argument → uses process.cwd(), display as '.'
|
|
28
|
-
* - Argument ending with facet.json → silently uses parent directory
|
|
29
|
-
* - Argument is a non-directory file → error
|
|
30
|
-
* - Directory doesn't exist + mustExist false → auto-creates it
|
|
31
|
-
* - Directory doesn't exist + mustExist true → error
|
|
32
|
-
* - facetMustExist true but no facet.json → error
|
|
33
|
-
*/
|
|
34
|
-
export async function resolveTargetDir(arg: string | undefined, opts: ResolveOptions): Promise<ResolveResult> {
|
|
35
|
-
const display = arg || '.'
|
|
36
|
-
|
|
37
|
-
// No argument → current directory
|
|
38
|
-
if (!arg) {
|
|
39
|
-
const dir = process.cwd()
|
|
40
|
-
|
|
41
|
-
if (opts.facetMustExist) {
|
|
42
|
-
const manifestExists = await Bun.file(join(dir, FACET_MANIFEST_FILE)).exists()
|
|
43
|
-
if (!manifestExists) {
|
|
44
|
-
return { ok: false, message: `No ${FACET_MANIFEST_FILE} found in ${display}` }
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return { ok: true, dir, display }
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Pointing to facet.json directly → use parent directory
|
|
52
|
-
if (arg === FACET_MANIFEST_FILE || arg.endsWith(`/${FACET_MANIFEST_FILE}`)) {
|
|
53
|
-
const dir = resolve(dirname(arg))
|
|
54
|
-
|
|
55
|
-
const dirStat = await stat(dir).catch(() => null)
|
|
56
|
-
if (!dirStat?.isDirectory()) {
|
|
57
|
-
return { ok: false, message: `Directory does not exist: ${dirname(arg)}` }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (opts.facetMustExist) {
|
|
61
|
-
const manifestExists = await Bun.file(join(dir, FACET_MANIFEST_FILE)).exists()
|
|
62
|
-
if (!manifestExists) {
|
|
63
|
-
return { ok: false, message: `No ${FACET_MANIFEST_FILE} found in ${display}` }
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return { ok: true, dir, display: dirname(arg) || '.' }
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Check if the path exists
|
|
71
|
-
const pathStat = await stat(arg).catch(() => null)
|
|
72
|
-
|
|
73
|
-
// Path exists but is a file, not a directory
|
|
74
|
-
if (pathStat && !pathStat.isDirectory()) {
|
|
75
|
-
return { ok: false, message: `Expected a directory, not a file: ${arg}` }
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Path doesn't exist
|
|
79
|
-
if (!pathStat) {
|
|
80
|
-
if (opts.mustExist) {
|
|
81
|
-
return { ok: false, message: `Directory does not exist: ${arg}` }
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Auto-create for commands that allow it (e.g., create)
|
|
85
|
-
await mkdir(arg, { recursive: true })
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const dir = resolve(arg)
|
|
89
|
-
|
|
90
|
-
if (opts.facetMustExist) {
|
|
91
|
-
const manifestExists = await Bun.file(join(dir, FACET_MANIFEST_FILE)).exists()
|
|
92
|
-
if (!manifestExists) {
|
|
93
|
-
return { ok: false, message: `No ${FACET_MANIFEST_FILE} found in ${display}` }
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return { ok: true, dir, display }
|
|
98
|
-
}
|
package/src/commands.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { buildCommand } from './commands/build.ts'
|
|
2
|
-
import { createCommand } from './commands/create/index.ts'
|
|
3
|
-
import { editCommand } from './commands/edit/index.ts'
|
|
4
|
-
|
|
5
|
-
export type FlagDef = {
|
|
6
|
-
type: 'boolean' | 'string'
|
|
7
|
-
description: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export type Command = {
|
|
11
|
-
name: string
|
|
12
|
-
description: string
|
|
13
|
-
usage?: string
|
|
14
|
-
flags?: Record<string, FlagDef>
|
|
15
|
-
run: (args: string[], flags: Record<string, unknown>) => Promise<number>
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function stubCommand(name: string, description: string): Command {
|
|
19
|
-
return {
|
|
20
|
-
name,
|
|
21
|
-
description,
|
|
22
|
-
run: async (_args, _flags) => {
|
|
23
|
-
console.log(`"${name}" is not yet implemented.`)
|
|
24
|
-
return 0
|
|
25
|
-
},
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const commands: Record<string, Command> = {
|
|
30
|
-
add: stubCommand('add', 'Add a facet to the project'),
|
|
31
|
-
build: buildCommand,
|
|
32
|
-
create: createCommand,
|
|
33
|
-
edit: editCommand,
|
|
34
|
-
info: stubCommand('info', 'Show information about a facet'),
|
|
35
|
-
install: stubCommand('install', 'Install all facets from the lockfile'),
|
|
36
|
-
list: stubCommand('list', 'List installed facets'),
|
|
37
|
-
publish: stubCommand('publish', 'Publish a facet to the registry'),
|
|
38
|
-
remove: stubCommand('remove', 'Remove a facet from the project'),
|
|
39
|
-
upgrade: stubCommand('upgrade', 'Upgrade installed facets'),
|
|
40
|
-
}
|
package/src/help.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { Command } from './commands.ts'
|
|
2
|
-
import { version } from './version.ts'
|
|
3
|
-
|
|
4
|
-
export function printGlobalHelp(commands: Record<string, Command>): void {
|
|
5
|
-
const entries = Object.values(commands)
|
|
6
|
-
const maxNameLength = Math.max(...entries.map((c) => c.name.length))
|
|
7
|
-
|
|
8
|
-
const lines = [
|
|
9
|
-
`facet v${version}`,
|
|
10
|
-
'',
|
|
11
|
-
'Usage: facet <command> [options]',
|
|
12
|
-
'',
|
|
13
|
-
'Commands:',
|
|
14
|
-
...entries.map((c) => ` ${c.name.padEnd(maxNameLength + 2)}${c.description}`),
|
|
15
|
-
'',
|
|
16
|
-
'Options:',
|
|
17
|
-
' --help Show help',
|
|
18
|
-
' --version Show version',
|
|
19
|
-
]
|
|
20
|
-
|
|
21
|
-
console.log(lines.join('\n'))
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function printCommandHelp(command: Command): void {
|
|
25
|
-
const usage = command.usage ? ` ${command.usage}` : ''
|
|
26
|
-
|
|
27
|
-
const lines = [`Usage: facet ${command.name}${usage} [options]`, '', ` ${command.description}`, '', 'Options:']
|
|
28
|
-
|
|
29
|
-
if (command.flags) {
|
|
30
|
-
const flagEntries = Object.entries(command.flags)
|
|
31
|
-
const maxFlagLength = Math.max(...flagEntries.map(([name]) => `--${name}`.length), '--help'.length)
|
|
32
|
-
|
|
33
|
-
for (const [name, def] of flagEntries) {
|
|
34
|
-
lines.push(` ${`--${name}`.padEnd(maxFlagLength + 4)}${def.description}`)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
lines.push(` ${'--help'.padEnd(maxFlagLength + 4)}Show help`)
|
|
38
|
-
} else {
|
|
39
|
-
lines.push(' --help Show help')
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
console.log(lines.join('\n'))
|
|
43
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { commands } from './commands.ts'
|
|
2
|
-
import { run } from './run.ts'
|
|
3
|
-
|
|
4
|
-
try {
|
|
5
|
-
const code = await run(process.argv.slice(2), commands)
|
|
6
|
-
process.exit(code)
|
|
7
|
-
} catch (error) {
|
|
8
|
-
console.error(error instanceof Error ? error.message : 'An unexpected error occurred.')
|
|
9
|
-
process.exit(2)
|
|
10
|
-
}
|
package/src/run.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { parse } from '@bomb.sh/args'
|
|
2
|
-
import type { Command } from './commands.ts'
|
|
3
|
-
import { printCommandHelp, printGlobalHelp } from './help.ts'
|
|
4
|
-
import { findClosestCommand } from './suggest.ts'
|
|
5
|
-
import { version } from './version.ts'
|
|
6
|
-
|
|
7
|
-
export async function run(argv: string[], commands: Record<string, Command>): Promise<number> {
|
|
8
|
-
const args = parse(argv, {
|
|
9
|
-
boolean: ['help', 'version'],
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
if (args.version) {
|
|
13
|
-
console.log(version)
|
|
14
|
-
return 0
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const commandName = String(args._[0] ?? '')
|
|
18
|
-
|
|
19
|
-
// No command given — show global help
|
|
20
|
-
if (!commandName) {
|
|
21
|
-
printGlobalHelp(commands)
|
|
22
|
-
return 0
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Explicit `help` command: `facets help` or `facets help build`
|
|
26
|
-
if (commandName === 'help') {
|
|
27
|
-
const subCommandName = String(args._[1] ?? '')
|
|
28
|
-
const subCommand = subCommandName ? commands[subCommandName] : undefined
|
|
29
|
-
if (subCommand) {
|
|
30
|
-
printCommandHelp(subCommand)
|
|
31
|
-
} else {
|
|
32
|
-
printGlobalHelp(commands)
|
|
33
|
-
}
|
|
34
|
-
return 0
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const command = commands[commandName]
|
|
38
|
-
|
|
39
|
-
if (!command) {
|
|
40
|
-
const suggestion = findClosestCommand(commandName, Object.keys(commands))
|
|
41
|
-
const message = suggestion
|
|
42
|
-
? `Unknown command "${commandName}". Did you mean "${suggestion}"?`
|
|
43
|
-
: `Unknown command "${commandName}".`
|
|
44
|
-
console.error(message)
|
|
45
|
-
return 1
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Per-command help: `facets build --help`
|
|
49
|
-
if (args.help) {
|
|
50
|
-
printCommandHelp(command)
|
|
51
|
-
return 0
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Build per-command flag parsing config
|
|
55
|
-
const booleanFlags: string[] = []
|
|
56
|
-
const stringFlags: string[] = []
|
|
57
|
-
|
|
58
|
-
if (command.flags) {
|
|
59
|
-
for (const [name, def] of Object.entries(command.flags)) {
|
|
60
|
-
if (def.type === 'boolean') booleanFlags.push(name)
|
|
61
|
-
else if (def.type === 'string') stringFlags.push(name)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Parse with per-command config
|
|
66
|
-
const parsed = parse(argv.slice(1), {
|
|
67
|
-
boolean: booleanFlags,
|
|
68
|
-
string: stringFlags,
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
// Build positional args and flags
|
|
72
|
-
const positionalArgs = parsed._.map(String)
|
|
73
|
-
const flags: Record<string, unknown> = {}
|
|
74
|
-
|
|
75
|
-
for (const name of [...booleanFlags, ...stringFlags]) {
|
|
76
|
-
if (parsed[name] !== undefined) {
|
|
77
|
-
flags[name] = parsed[name]
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return command.run(positionalArgs, flags)
|
|
82
|
-
}
|
package/src/suggest.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
function levenshtein(a: string, b: string): number {
|
|
2
|
-
const m = a.length
|
|
3
|
-
const n = b.length
|
|
4
|
-
const row: number[] = []
|
|
5
|
-
|
|
6
|
-
for (let j = 0; j <= n; j++) row.push(j)
|
|
7
|
-
|
|
8
|
-
for (let i = 1; i <= m; i++) {
|
|
9
|
-
let prev = row[0] ?? 0
|
|
10
|
-
row[0] = i
|
|
11
|
-
for (let j = 1; j <= n; j++) {
|
|
12
|
-
const current = row[j] ?? 0
|
|
13
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1
|
|
14
|
-
row[j] = Math.min(current + 1, (row[j - 1] ?? 0) + 1, prev + cost)
|
|
15
|
-
prev = current
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return row[n] ?? 0
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function findClosestCommand(input: string, commandNames: string[]): string | undefined {
|
|
23
|
-
let best: string | undefined
|
|
24
|
-
let bestDistance = Infinity
|
|
25
|
-
|
|
26
|
-
for (const name of commandNames) {
|
|
27
|
-
const distance = levenshtein(input, name)
|
|
28
|
-
if (distance < bestDistance) {
|
|
29
|
-
bestDistance = distance
|
|
30
|
-
best = name
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return bestDistance <= 3 ? best : undefined
|
|
35
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink'
|
|
2
|
-
|
|
3
|
-
export function truncateDescription(text: string, maxLen = 50): string {
|
|
4
|
-
const lines = text.split(/\r?\n/)
|
|
5
|
-
const firstLine = lines[0] ?? text
|
|
6
|
-
const hasMore = lines.length > 1
|
|
7
|
-
if (firstLine.length <= maxLen) return hasMore ? `${firstLine}...` : firstLine
|
|
8
|
-
return `${firstLine.slice(0, maxLen)}...`
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function AssetDescription({ description }: { description: string }) {
|
|
12
|
-
return (
|
|
13
|
-
<Box marginLeft={4}>
|
|
14
|
-
<Text dimColor>{truncateDescription(description)}</Text>
|
|
15
|
-
</Box>
|
|
16
|
-
)
|
|
17
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { Box, Text, useInput } from 'ink'
|
|
2
|
-
import { useEffect, useState } from 'react'
|
|
3
|
-
import { useFocusMode } from '../context/focus-mode-context.ts'
|
|
4
|
-
import { THEME } from '../theme.ts'
|
|
5
|
-
|
|
6
|
-
export type AssetField = 'name' | 'description'
|
|
7
|
-
|
|
8
|
-
export function AssetFieldPicker({
|
|
9
|
-
name,
|
|
10
|
-
description,
|
|
11
|
-
initialField,
|
|
12
|
-
onChoose,
|
|
13
|
-
onCancel,
|
|
14
|
-
}: {
|
|
15
|
-
name: string
|
|
16
|
-
description: string
|
|
17
|
-
initialField?: AssetField
|
|
18
|
-
onChoose: (field: AssetField) => void
|
|
19
|
-
onCancel: () => void
|
|
20
|
-
}) {
|
|
21
|
-
const { setMode } = useFocusMode()
|
|
22
|
-
const [field, setField] = useState<AssetField>(initialField ?? 'name')
|
|
23
|
-
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
setMode('field-revision')
|
|
26
|
-
return () => setMode('form-navigation')
|
|
27
|
-
}, [setMode])
|
|
28
|
-
|
|
29
|
-
useInput((_input, key) => {
|
|
30
|
-
if (key.upArrow) setField('name')
|
|
31
|
-
if (key.downArrow) setField('description')
|
|
32
|
-
if (key.escape) onCancel()
|
|
33
|
-
if (key.return) onChoose(field)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<Box flexDirection="column">
|
|
38
|
-
<Box gap={1} marginLeft={2}>
|
|
39
|
-
{field === 'name' ? (
|
|
40
|
-
<>
|
|
41
|
-
<Text color={THEME.primary} bold>
|
|
42
|
-
▸
|
|
43
|
-
</Text>
|
|
44
|
-
<Text color={THEME.primary}>{name}</Text>
|
|
45
|
-
<Text color={THEME.hint}>
|
|
46
|
-
<Text color={THEME.keyword}>↑↓</Text> select · <Text color={THEME.keyword}>Enter</Text> edit ·{' '}
|
|
47
|
-
<Text color={THEME.keyword}>Esc</Text> back
|
|
48
|
-
</Text>
|
|
49
|
-
</>
|
|
50
|
-
) : (
|
|
51
|
-
<>
|
|
52
|
-
<Text color={THEME.success}>•</Text>
|
|
53
|
-
<Text>{name}</Text>
|
|
54
|
-
</>
|
|
55
|
-
)}
|
|
56
|
-
</Box>
|
|
57
|
-
<Box gap={1} marginLeft={2}>
|
|
58
|
-
{field === 'description' ? (
|
|
59
|
-
<>
|
|
60
|
-
<Text color={THEME.primary} bold>
|
|
61
|
-
▸
|
|
62
|
-
</Text>
|
|
63
|
-
<Text color={THEME.primary}>{description}</Text>
|
|
64
|
-
<Text color={THEME.hint}>
|
|
65
|
-
<Text color={THEME.keyword}>↑↓</Text> select · <Text color={THEME.keyword}>Enter</Text> edit ·{' '}
|
|
66
|
-
<Text color={THEME.keyword}>Esc</Text> back
|
|
67
|
-
</Text>
|
|
68
|
-
</>
|
|
69
|
-
) : (
|
|
70
|
-
<>
|
|
71
|
-
<Text> </Text>
|
|
72
|
-
<Text dimColor>{description}</Text>
|
|
73
|
-
</>
|
|
74
|
-
)}
|
|
75
|
-
</Box>
|
|
76
|
-
</Box>
|
|
77
|
-
)
|
|
78
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { Box, Text, useInput } from 'ink'
|
|
2
|
-
import TextInput from 'ink-text-input'
|
|
3
|
-
import { THEME } from '../theme.ts'
|
|
4
|
-
|
|
5
|
-
export function AssetInlineInput({
|
|
6
|
-
value,
|
|
7
|
-
placeholder,
|
|
8
|
-
error,
|
|
9
|
-
isFocused,
|
|
10
|
-
onChange,
|
|
11
|
-
validate,
|
|
12
|
-
onError,
|
|
13
|
-
onSubmit,
|
|
14
|
-
onCancel,
|
|
15
|
-
onDownArrow,
|
|
16
|
-
}: {
|
|
17
|
-
id: string
|
|
18
|
-
value: string
|
|
19
|
-
placeholder?: string
|
|
20
|
-
error: string
|
|
21
|
-
isFocused: boolean
|
|
22
|
-
onChange: (value: string) => void
|
|
23
|
-
validate?: (value: string) => string | undefined
|
|
24
|
-
onError: (error: string) => void
|
|
25
|
-
onSubmit: (name: string) => void
|
|
26
|
-
onCancel: () => void
|
|
27
|
-
onDownArrow?: () => void
|
|
28
|
-
}) {
|
|
29
|
-
useInput(
|
|
30
|
-
(_input, key) => {
|
|
31
|
-
if (!isFocused) return
|
|
32
|
-
if (key.return) {
|
|
33
|
-
const name = value || placeholder || ''
|
|
34
|
-
if (!name) {
|
|
35
|
-
onCancel()
|
|
36
|
-
return
|
|
37
|
-
}
|
|
38
|
-
if (validate) {
|
|
39
|
-
const err = validate(name)
|
|
40
|
-
if (err) {
|
|
41
|
-
onError(err)
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
onSubmit(name)
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
if (key.escape) {
|
|
49
|
-
onCancel()
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
if (key.downArrow && onDownArrow) {
|
|
53
|
-
onDownArrow()
|
|
54
|
-
return
|
|
55
|
-
}
|
|
56
|
-
if (key.tab && !value && placeholder) {
|
|
57
|
-
onChange(placeholder)
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
{ isActive: isFocused },
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<Box marginLeft={2} gap={1}>
|
|
66
|
-
<Text color={THEME.tertiary}>{'>'}</Text>
|
|
67
|
-
<TextInput value={value} onChange={onChange} placeholder={placeholder} focus={isFocused} />
|
|
68
|
-
{error ? (
|
|
69
|
-
<Text color={THEME.warning}>· {error}</Text>
|
|
70
|
-
) : value ? (
|
|
71
|
-
<Text color={THEME.hint}>
|
|
72
|
-
· <Text color={THEME.keyword}>Enter</Text> save ·{' '}
|
|
73
|
-
{onDownArrow && (
|
|
74
|
-
<>
|
|
75
|
-
<Text color={THEME.keyword}>↓</Text> description ·{' '}
|
|
76
|
-
</>
|
|
77
|
-
)}
|
|
78
|
-
<Text color={THEME.keyword}>Esc</Text> revert
|
|
79
|
-
</Text>
|
|
80
|
-
) : placeholder ? (
|
|
81
|
-
<Text color={THEME.hint}>
|
|
82
|
-
· <Text color={THEME.keyword}>Tab</Text> to fill · <Text color={THEME.keyword}>Escape</Text> to cancel
|
|
83
|
-
</Text>
|
|
84
|
-
) : (
|
|
85
|
-
<Text color={THEME.hint}>
|
|
86
|
-
· <Text color={THEME.keyword}>Enter</Text> to add · <Text color={THEME.keyword}>Escape</Text> to cancel
|
|
87
|
-
</Text>
|
|
88
|
-
)}
|
|
89
|
-
</Box>
|
|
90
|
-
)
|
|
91
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { Box, Text, useInput } from 'ink'
|
|
2
|
-
import { THEME } from '../theme.ts'
|
|
3
|
-
|
|
4
|
-
export function AssetItem({
|
|
5
|
-
name,
|
|
6
|
-
isFocused,
|
|
7
|
-
onEdit,
|
|
8
|
-
onRemove,
|
|
9
|
-
}: {
|
|
10
|
-
id: string
|
|
11
|
-
name: string
|
|
12
|
-
isFocused: boolean
|
|
13
|
-
onEdit: () => void
|
|
14
|
-
onRemove: () => void
|
|
15
|
-
}) {
|
|
16
|
-
useInput(
|
|
17
|
-
(_input, key) => {
|
|
18
|
-
if (key.return) onEdit()
|
|
19
|
-
if (key.delete || key.backspace) onRemove()
|
|
20
|
-
},
|
|
21
|
-
{ isActive: isFocused },
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<Box gap={1} marginLeft={2}>
|
|
26
|
-
{isFocused ? (
|
|
27
|
-
<>
|
|
28
|
-
<Text color={THEME.primary} bold>
|
|
29
|
-
▸
|
|
30
|
-
</Text>
|
|
31
|
-
<Text color={THEME.primary}>{name}</Text>
|
|
32
|
-
<Text color={THEME.hint}>
|
|
33
|
-
<Text color={THEME.keyword}>Enter</Text> edit name · <Text color={THEME.keyword}>Del</Text> remove
|
|
34
|
-
</Text>
|
|
35
|
-
</>
|
|
36
|
-
) : (
|
|
37
|
-
<>
|
|
38
|
-
<Text color={THEME.success}>•</Text>
|
|
39
|
-
<Text>{name}</Text>
|
|
40
|
-
</>
|
|
41
|
-
)}
|
|
42
|
-
</Box>
|
|
43
|
-
)
|
|
44
|
-
}
|