incur 0.3.12 → 0.3.14

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/src/Skill.ts CHANGED
@@ -5,7 +5,8 @@ import * as Schema from './Schema.js'
5
5
 
6
6
  /** Information about a single command, passed to `generate()`. */
7
7
  export type CommandInfo = {
8
- name: string
8
+ /** Command name (subcommand path). Omit for root commands. */
9
+ name?: string | undefined
9
10
  description?: string | undefined
10
11
  args?: z.ZodObject<any> | undefined
11
12
  env?: z.ZodObject<any> | undefined
@@ -48,7 +49,7 @@ export function index(
48
49
 
49
50
  /** @internal Builds a command signature with arg placeholders. */
50
51
  function buildSignature(cli: string, cmd: CommandInfo): string {
51
- const base = `${cli} ${cmd.name}`
52
+ const base = !cmd.name ? cli : `${cli} ${cmd.name}`
52
53
  if (!cmd.args) return base
53
54
  const shape = cmd.args.shape as Record<string, z.ZodType>
54
55
  const json = Schema.toJsonSchema(cmd.args)
@@ -70,14 +71,16 @@ export function generate(
70
71
  let lastGroup: string | undefined
71
72
 
72
73
  for (const cmd of commands) {
73
- const segment = cmd.name.split(' ')[0]!
74
+ const segment = !cmd.name ? '' : cmd.name.split(' ')[0]!
74
75
  if (segment !== lastGroup) {
75
76
  lastGroup = segment
76
- const desc = groups.get(segment)
77
- const heading = desc ? `## ${name} ${segment}\n\n${desc}` : `## ${name} ${segment}`
78
- sections.push(heading)
77
+ if (segment) {
78
+ const desc = groups.get(segment)
79
+ const heading = desc ? `## ${name} ${segment}\n\n${desc}` : `## ${name} ${segment}`
80
+ sections.push(heading)
81
+ }
79
82
  }
80
- sections.push(renderCommandBody(name, cmd, 3))
83
+ sections.push(renderCommandBody(name, cmd, segment ? 3 : 2))
81
84
  }
82
85
 
83
86
  return sections.join('\n\n')
@@ -94,6 +97,13 @@ export function split(
94
97
 
95
98
  const buckets = new Map<string, CommandInfo[]>()
96
99
  for (const cmd of commands) {
100
+ if (!cmd.name) {
101
+ const key = slugify(name)
102
+ const bucket = buckets.get(key) ?? []
103
+ bucket.push(cmd)
104
+ buckets.set(key, bucket)
105
+ continue
106
+ }
97
107
  const segments = cmd.name.split(' ')
98
108
  const key = segments.slice(0, depth).join('-')
99
109
  const bucket = buckets.get(key) ?? []
@@ -104,8 +114,10 @@ export function split(
104
114
  return [...buckets.entries()]
105
115
  .sort(([a], [b]) => a.localeCompare(b))
106
116
  .map(([dir, cmds]) => {
107
- const prefix = cmds[0]!.name.split(' ').slice(0, depth).join(' ')
108
- return { dir, content: renderGroup(name, `${name} ${prefix}`, cmds, groups, prefix) }
117
+ const first = cmds[0]!
118
+ const prefix = !first.name ? '' : first.name.split(' ').slice(0, depth).join(' ')
119
+ const title = prefix ? `${name} ${prefix}` : name
120
+ return { dir, content: renderGroup(name, title, cmds, groups, prefix || undefined) }
109
121
  })
110
122
  }
111
123
 
@@ -127,12 +139,7 @@ function renderGroup(
127
139
  ? `${descParts.join('. ')}. Run \`${title} --help\` for usage details.`
128
140
  : `Run \`${title} --help\` for usage details.`
129
141
 
130
- const slug = title
131
- .toLowerCase()
132
- .replace(/[^a-z0-9-]+/g, '-')
133
- .replace(/-{2,}/g, '-')
134
- .replace(/^-|-$/g, '')
135
- const fm = ['---', `name: ${slug}`]
142
+ const fm = ['---', `name: ${slugify(title)}`]
136
143
  fm.push(`description: ${description}`)
137
144
  fm.push(`requires_bin: ${cli}`)
138
145
  fm.push(`command: ${title}`, '---')
@@ -143,7 +150,7 @@ function renderGroup(
143
150
 
144
151
  /** @internal Renders a command's heading and sections without frontmatter. */
145
152
  function renderCommandBody(cli: string, cmd: CommandInfo, level = 1): string {
146
- const fullName = `${cli} ${cmd.name}`
153
+ const fullName = !cmd.name ? cli : `${cli} ${cmd.name}`
147
154
  const sections: string[] = []
148
155
  const h = (n: number) => '#'.repeat(n)
149
156
 
@@ -287,6 +294,15 @@ function schemaToTable(schema: Record<string, unknown>, prefix = ''): string | u
287
294
  return `| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n${rows.join('\n')}`
288
295
  }
289
296
 
297
+ /** @internal Converts a string to a lowercase slug (e.g. `"my-cli"` → `"my-cli"`, `"My Tool"` → `"my-tool"`). */
298
+ function slugify(s: string): string {
299
+ return s
300
+ .toLowerCase()
301
+ .replace(/[^a-z0-9-]+/g, '-')
302
+ .replace(/-{2,}/g, '-')
303
+ .replace(/^-|-$/g, '')
304
+ }
305
+
290
306
  /** @internal Resolves a simple type name from a JSON Schema property. */
291
307
  function resolveTypeName(prop: Record<string, unknown> | undefined): string {
292
308
  if (!prop) return 'unknown'
package/src/Skillgen.ts CHANGED
@@ -62,5 +62,5 @@ function collectEntries(
62
62
  result.push(cmd)
63
63
  }
64
64
  }
65
- return result.sort((a, b) => a.name.localeCompare(b.name))
65
+ return result.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''))
66
66
  }
package/src/SyncSkills.ts CHANGED
@@ -18,7 +18,7 @@ export async function sync(
18
18
 
19
19
  const groups = new Map<string, string>()
20
20
  if (description) groups.set(name, description)
21
- const entries = collectEntries(commands, [], groups)
21
+ const entries = collectEntries(commands, [], groups, options.rootCommand)
22
22
  const files = Skill.split(name, entries, depth, groups)
23
23
 
24
24
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), `incur-skills-${name}-`))
@@ -70,7 +70,7 @@ export async function sync(
70
70
  }
71
71
 
72
72
  // Write skills hash + names for staleness detection
73
- const hashEntries = collectEntries(commands, [])
73
+ const hashEntries = collectEntries(commands, [], undefined, options.rootCommand)
74
74
  writeMeta(name, Skill.hash(hashEntries), [...currentNames])
75
75
 
76
76
  return { skills, paths, agents }
@@ -92,6 +92,18 @@ export declare namespace sync {
92
92
  global?: boolean | undefined
93
93
  /** Glob patterns for directories containing SKILL.md files to include (e.g. `"skills/*"`, `"my-skill"`). Skill name is the parent directory name. */
94
94
  include?: string[] | undefined
95
+ /** Root command definition (when the CLI itself has a `run` handler). */
96
+ rootCommand?:
97
+ | {
98
+ description?: string | undefined
99
+ args?: any
100
+ env?: any
101
+ hint?: string | undefined
102
+ options?: any
103
+ output?: any
104
+ examples?: any[] | undefined
105
+ }
106
+ | undefined
95
107
  }
96
108
  /** Result of a sync operation. */
97
109
  type Result = {
@@ -118,8 +130,31 @@ function collectEntries(
118
130
  commands: Map<string, any>,
119
131
  prefix: string[],
120
132
  groups: Map<string, string> = new Map(),
133
+ rootCommand?:
134
+ | {
135
+ description?: string | undefined
136
+ args?: any
137
+ env?: any
138
+ hint?: string | undefined
139
+ options?: any
140
+ output?: any
141
+ examples?: any[] | undefined
142
+ }
143
+ | undefined,
121
144
  ): Skill.CommandInfo[] {
122
145
  const result: Skill.CommandInfo[] = []
146
+ if (rootCommand) {
147
+ const cmd: Skill.CommandInfo = {}
148
+ if (rootCommand.description) cmd.description = rootCommand.description
149
+ if (rootCommand.args) cmd.args = rootCommand.args
150
+ if (rootCommand.env) cmd.env = rootCommand.env
151
+ if (rootCommand.hint) cmd.hint = rootCommand.hint
152
+ if (rootCommand.options) cmd.options = rootCommand.options
153
+ if (rootCommand.output) cmd.output = rootCommand.output
154
+ const examples = formatExamples(rootCommand.examples)
155
+ if (examples) cmd.examples = examples
156
+ result.push(cmd)
157
+ }
123
158
  for (const [name, entry] of commands) {
124
159
  const entryPath = [...prefix, name]
125
160
  if ('_group' in entry && entry._group) {
@@ -144,7 +179,7 @@ function collectEntries(
144
179
  result.push(cmd)
145
180
  }
146
181
  }
147
- return result.sort((a, b) => a.name.localeCompare(b.name))
182
+ return result.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''))
148
183
  }
149
184
 
150
185
  /** Resolves the package root from the executing bin script (`process.argv[1]`). Walks up from the bin's directory looking for `package.json`. Falls back to `process.cwd()`. */