@newlogic-digital/cli 1.5.0-next.3 → 1.5.0-next.5

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/index.mjs CHANGED
@@ -3,10 +3,11 @@
3
3
  import init from './src/commands/init/index.mjs'
4
4
  import cms from './src/commands/cms/index.mjs'
5
5
  import blocks from './src/commands/blocks/index.mjs'
6
+ import skills from './src/commands/skills/index.mjs'
6
7
  import { styleText } from 'node:util'
7
8
  import { version, name } from './src/utils.mjs'
8
9
 
9
- const knownCommands = ['init', 'cms', 'blocks']
10
+ const knownCommands = ['init', 'cms', 'blocks', 'skills']
10
11
 
11
12
  function normalizeOptionName(name) {
12
13
  return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
@@ -149,15 +150,19 @@ if (!command) {
149
150
 
150
151
  -- blocks --
151
152
  ${styleText('green', 'newlogic blocks list')} - Lists all available installable blocks with descriptions
152
- ${styleText('green', 'newlogic blocks add')} ${styleText('yellow', '<name...>')} - Installs one or more blocks by kebab-case or PascalCase name
153
+ ${styleText('green', 'newlogic blocks add')} ${styleText('yellow', '<name[@variant]...>')} - Installs one or more blocks by kebab-case or PascalCase name
153
154
  ${styleText('green', 'newlogic blocks remove')} ${styleText('yellow', '<name...>')} - Removes one or more blocks and orphaned dependencies
154
155
  ${styleText('green', 'newlogic blocks update')} - Reinstalls all configured blocks from ${styleText('yellow', 'newlogic.config.json')}
155
156
  ${styleText('green', 'newlogic blocks add')} ${styleText('yellow', 'about-accordion')}
156
157
  ${styleText('green', 'newlogic blocks add')} ${styleText('yellow', 'AboutAccordion')}
158
+ ${styleText('green', 'newlogic blocks add')} ${styleText('yellow', 'header-nav-left@stimulus')}
157
159
  ${styleText('green', 'newlogic blocks add')} ${styleText('yellow', 'header-nav-left contact-info-card hero-floating-text')}
158
160
  ${styleText('green', 'newlogic blocks remove')} ${styleText('yellow', 'about-accordion')}
159
161
  ${styleText('green', 'newlogic blocks remove')} ${styleText('yellow', 'header-nav-left hero-floating-text')}
160
162
  ${styleText('green', 'newlogic blocks update')}
163
+
164
+ -- skills --
165
+ ${styleText('green', 'newlogic skills install')} - Installs the bundled ${styleText('yellow', 'newlogic-cli')} skill via ${styleText('blue', 'npx skills add')}
161
166
  `)
162
167
 
163
168
  process.exit(0)
@@ -187,6 +192,13 @@ if (command === 'blocks') {
187
192
  await blocks(action, names)
188
193
  }
189
194
 
195
+ if (command === 'skills') {
196
+ const { positionals } = parseCommandArgs(rawArgs.slice(1))
197
+ const action = positionals[0]
198
+
199
+ await skills(action)
200
+ }
201
+
190
202
  if (command && !knownCommands.includes(command)) {
191
203
  printUnknownCommand(command)
192
204
  process.exit(1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newlogic-digital/cli",
3
- "version": "1.5.0-next.3",
3
+ "version": "1.5.0-next.5",
4
4
  "main": "index.mjs",
5
5
  "bin": {
6
6
  "newlogic-cli": "index.mjs",
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: newlogic
2
+ name: newlogic-cli
3
3
  description: Use the installed Newlogic CLI when Codex needs to run `newlogic`, scaffold `@newlogic-digital/ui` or `@newlogic-digital/cms` projects, prepare CMS templates and components, or manage installable blocks through `newlogic.config.json`. Trigger this skill for tasks involving the commands `init`, `cms`, or `blocks`, especially when an agent should run the CLI safely and non-interactively.
4
4
  ---
5
5
 
@@ -0,0 +1,6 @@
1
+ interface:
2
+ display_name: "Newlogic CLI"
3
+ short_description: "Use installed Newlogic CLI for init, cms, and blocks."
4
+ icon_small: "./assets/favicon.svg"
5
+ icon_large: "./assets/maskable.png"
6
+ default_prompt: "Use $newlogic-cli to run Newlogic init, cms, or blocks commands safely and non-interactively."
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120">
2
+ <path fill="#000" d="M89,0,62.68,26.36,36.32,0H0V120H31L57.32,93.64,83.68,120H120V0Zm0,8.53V52.72l-22.1-22.1Zm6,58.75V6H114V109.7L10.3,6H33.82ZM31,111.47V67.28l22.1,22.1Zm-6-58.75V114H6V10.29L109.7,114H86.18Z"/>
3
+ </svg>
@@ -134,13 +134,14 @@ function printBlocksUsage() {
134
134
  styleText(['white', 'bold'], 'Usage:'),
135
135
  '',
136
136
  ` ${styleText('green', 'newlogic blocks list')} - Lists all available blocks with descriptions`,
137
- ` ${styleText('green', 'newlogic blocks add')} ${styleText('yellow', '<name...>')} - Adds one or more blocks by kebab-case or PascalCase name`,
137
+ ` ${styleText('green', 'newlogic blocks add')} ${styleText('yellow', '<name[@variant]...>')} - Adds one or more blocks by kebab-case or PascalCase name`,
138
138
  ` ${styleText('green', 'newlogic blocks remove')} ${styleText('yellow', '<name...>')} - Removes one or more blocks and orphaned dependencies`,
139
139
  ` ${styleText('green', 'newlogic blocks update')} - Reinstalls all configured blocks from ${styleText('yellow', 'newlogic.config.json')}`,
140
140
  '',
141
141
  styleText(['white', 'bold'], 'Examples:'),
142
142
  '',
143
143
  ` ${styleText('green', 'newlogic blocks add')} ${styleText('yellow', 'header-nav-left contact-info-card hero-floating-text')}`,
144
+ ` ${styleText('green', 'newlogic blocks add')} ${styleText('yellow', 'header-nav-left@stimulus')}`,
144
145
  ` ${styleText('green', 'newlogic blocks remove')} ${styleText('yellow', 'header-nav-left hero-floating-text')}`,
145
146
  ].join('\n'))
146
147
  }
@@ -83,6 +83,51 @@ function assertBlockName(name, label = 'block name') {
83
83
  }
84
84
  }
85
85
 
86
+ function parseBlockSpecifier(rawValue, {
87
+ label = 'block name',
88
+ allowObject = false,
89
+ } = {}) {
90
+ if (allowObject && rawValue && typeof rawValue === 'object' && !Array.isArray(rawValue)) {
91
+ const name = normalizeBlockName(rawValue.name)
92
+ const variant = rawValue.variant == null ? '' : `${rawValue.variant}`.trim()
93
+
94
+ assertBlockName(name, 'configured block name')
95
+
96
+ if (rawValue.variant != null && !variant) {
97
+ throw new Error(`Configured block "${name}" has an empty variant`)
98
+ }
99
+
100
+ return {
101
+ name,
102
+ variant: variant || undefined,
103
+ variantExplicit: rawValue.variant != null,
104
+ }
105
+ }
106
+
107
+ const value = `${rawValue ?? ''}`.trim()
108
+
109
+ if (!value) {
110
+ throw new Error(`Missing ${label}`)
111
+ }
112
+
113
+ const variantSeparator = value.indexOf('@')
114
+ const rawName = variantSeparator === -1 ? value : value.slice(0, variantSeparator)
115
+ const rawVariant = variantSeparator === -1 ? '' : value.slice(variantSeparator + 1).trim()
116
+ const name = normalizeBlockName(rawName)
117
+
118
+ assertBlockName(name, label)
119
+
120
+ if (variantSeparator !== -1 && !rawVariant) {
121
+ throw new Error(`Invalid ${label}: "${value}"`)
122
+ }
123
+
124
+ return {
125
+ name,
126
+ variant: rawVariant || undefined,
127
+ variantExplicit: variantSeparator !== -1,
128
+ }
129
+ }
130
+
86
131
  function toBlockKey(block) {
87
132
  return `${block.name}@${block.variant}`
88
133
  }
@@ -151,20 +196,14 @@ function loadProjectConfig(projectRoot) {
151
196
  }
152
197
 
153
198
  const blocks = blocksRaw.map((entry, index) => {
154
- if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
199
+ if (typeof entry !== 'string' && (!entry || typeof entry !== 'object' || Array.isArray(entry))) {
155
200
  throw new Error(`Invalid block config entry at index ${index}`)
156
201
  }
157
202
 
158
- const name = normalizeBlockName(entry.name)
159
- const variant = `${entry.variant ?? ''}`.trim()
160
-
161
- assertBlockName(name, 'configured block name')
162
-
163
- if (!variant) {
164
- throw new Error(`Configured block "${name}" is missing a variant`)
165
- }
166
-
167
- return { name, variant }
203
+ return parseBlockSpecifier(entry, {
204
+ label: `block config entry at index ${index}`,
205
+ allowObject: true,
206
+ })
168
207
  })
169
208
 
170
209
  return { configPath, config, blocks: dedupeExplicitBlocks(blocks) }
@@ -173,7 +212,7 @@ function loadProjectConfig(projectRoot) {
173
212
  function saveProjectConfig(configPath, config, blocks) {
174
213
  const nextConfig = {
175
214
  ...config,
176
- blocks,
215
+ blocks: blocks.map(block => block.variantExplicit ? `${block.name}@${block.variant}` : block.name),
177
216
  }
178
217
 
179
218
  fs.writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`)
@@ -194,15 +233,25 @@ function normalizeRequestedBlockNames(rawNames, label = 'block name') {
194
233
  throw new Error(`Missing ${label}`)
195
234
  }
196
235
 
197
- const normalizedNames = rawNames.map((rawName) => {
198
- const name = normalizeBlockName(rawName)
236
+ const normalizedNames = rawNames
237
+ .map(rawName => parseBlockSpecifier(rawName, { label }).name)
199
238
 
200
- assertBlockName(name, label)
239
+ return [...new Set(normalizedNames)]
240
+ }
201
241
 
202
- return name
203
- })
242
+ function normalizeRequestedBlocks(rawNames, label = 'block name') {
243
+ if (!Array.isArray(rawNames) || rawNames.length === 0) {
244
+ throw new Error(`Missing ${label}`)
245
+ }
204
246
 
205
- return [...new Set(normalizedNames)]
247
+ const blocks = rawNames.map(rawName => parseBlockSpecifier(rawName, { label }))
248
+ const byName = new Map()
249
+
250
+ for (const block of blocks) {
251
+ byName.set(block.name, block)
252
+ }
253
+
254
+ return [...byName.values()]
206
255
  }
207
256
 
208
257
  async function createMetaValidator(repository) {
@@ -488,16 +537,21 @@ export function createBlocksService({
488
537
  }
489
538
 
490
539
  async function addBlocks(rawNames) {
491
- const names = normalizeRequestedBlockNames(rawNames)
540
+ const requestedBlocks = normalizeRequestedBlocks(rawNames)
492
541
  const rootBlocks = []
493
542
 
494
- for (const name of names) {
495
- const meta = await getMeta(name)
543
+ for (const requestedBlock of requestedBlocks) {
544
+ const meta = await getMeta(requestedBlock.name)
496
545
 
497
546
  rootBlocks.push({
498
- name,
499
- variant: meta.install.defaultVariant,
547
+ name: requestedBlock.name,
548
+ variant: requestedBlock.variant,
549
+ variantExplicit: requestedBlock.variantExplicit,
500
550
  })
551
+
552
+ if (requestedBlock.variant && !meta.install?.variants?.[requestedBlock.variant]) {
553
+ throw new Error(`Variant "${requestedBlock.variant}" is not available for block "${requestedBlock.name}"`)
554
+ }
501
555
  }
502
556
 
503
557
  const plan = await resolveInstallPlan(rootBlocks)
@@ -506,12 +560,12 @@ export function createBlocksService({
506
560
 
507
561
  const { configPath, config, blocks } = loadProjectConfig(projectRoot)
508
562
  const nextBlocks = dedupeExplicitBlocks([
509
- ...blocks.filter(block => !names.includes(block.name)),
563
+ ...blocks.filter(block => !requestedBlocks.some(requestedBlock => requestedBlock.name === block.name)),
510
564
  ...rootBlocks,
511
565
  ])
512
566
 
513
567
  saveProjectConfig(configPath, config, nextBlocks)
514
- logger.info(`added ${rootBlocks.map(block => `${block.name}@${block.variant}`).join(', ')}`)
568
+ logger.info(`added ${rootBlocks.map(block => block.variantExplicit ? `${block.name}@${block.variant}` : block.name).join(', ')}`)
515
569
 
516
570
  return {
517
571
  blocks: rootBlocks,
@@ -0,0 +1,62 @@
1
+ import childProcess from 'node:child_process'
2
+ import fs from 'node:fs'
3
+ import { styleText } from 'node:util'
4
+ import { packageRoot, resolveInside } from '../../utils.mjs'
5
+
6
+ function label(color, text) {
7
+ return styleText([color, 'bold'], text)
8
+ }
9
+
10
+ function printSkillsUsage() {
11
+ console.log([
12
+ styleText(['blue', 'bold'], 'newlogic skills'),
13
+ '',
14
+ styleText(['white', 'bold'], 'Usage:'),
15
+ '',
16
+ ` ${styleText('green', 'newlogic skills install')} - Installs the bundled ${styleText('yellow', 'newlogic-cli')} skill`,
17
+ '',
18
+ styleText(['white', 'bold'], 'Notes:'),
19
+ '',
20
+ ` ${styleText('dim', 'Resolves the installed CLI location automatically and runs:')}`,
21
+ ` ${styleText('cyan', 'npx skills add <resolved-path>/skills/newlogic-cli')}`,
22
+ ].join('\n'))
23
+ }
24
+
25
+ function getBundledSkillPath(skillName = 'newlogic-cli') {
26
+ return resolveInside(packageRoot, 'skills', skillName)
27
+ }
28
+
29
+ function installBundledSkill(skillName = 'newlogic-cli', options = {}) {
30
+ const { execFileSync = childProcess.execFileSync } = options
31
+ const skillPath = getBundledSkillPath(skillName)
32
+
33
+ if (!fs.existsSync(skillPath)) {
34
+ throw new Error(`Bundled skill "${skillName}" not found at ${skillPath}`)
35
+ }
36
+
37
+ execFileSync('npx', ['skills', 'add', skillPath], { stdio: 'inherit' })
38
+
39
+ return skillPath
40
+ }
41
+
42
+ export { getBundledSkillPath, installBundledSkill }
43
+
44
+ export default async function skills(action) {
45
+ if (!action || action === 'help' || action === '--help') {
46
+ printSkillsUsage()
47
+ return
48
+ }
49
+
50
+ try {
51
+ if (action === 'install') {
52
+ installBundledSkill()
53
+ return
54
+ }
55
+
56
+ throw new Error(`Unknown skills action "${action}"`)
57
+ }
58
+ catch (error) {
59
+ console.log(`${label('red', 'error')} ${error.message}`)
60
+ process.exit(1)
61
+ }
62
+ }
package/src/utils.mjs CHANGED
@@ -3,7 +3,8 @@ import fs from 'fs'
3
3
  import path from 'path'
4
4
  import { fileURLToPath } from 'url'
5
5
 
6
- const { version, name } = JSON.parse(fs.readFileSync(path.resolve(path.dirname((fileURLToPath(import.meta.url))), '../package.json')).toString())
6
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
7
+ const { version, name } = JSON.parse(fs.readFileSync(path.resolve(packageRoot, 'package.json')).toString())
7
8
 
8
9
  const execSync = (cmd) => {
9
10
  try {
@@ -45,4 +46,4 @@ function resolveInside(rootDir, ...segments) {
45
46
  return nextPath
46
47
  }
47
48
 
48
- export { execSync, stripIndent, resolveInside, version, name }
49
+ export { execSync, stripIndent, resolveInside, version, name, packageRoot }
@@ -1,4 +0,0 @@
1
- interface:
2
- display_name: "Newlogic CLI"
3
- short_description: "Use the local Newlogic CLI safely and non-interactively."
4
- default_prompt: "Use this skill when you need to inspect or run the Newlogic CLI in this repository."