create-semaphor-app 0.1.3 → 0.1.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/README.md CHANGED
@@ -7,8 +7,9 @@ npx create-semaphor-app@latest
7
7
  ```
8
8
 
9
9
  The CLI scaffolds the public Semaphor Data App Starter, installs local
10
- dependencies by default, and can optionally install the Semaphor Agent Plugin
11
- for detected Codex and Claude Code installations.
10
+ dependencies by default, installs the production Semaphor shadcn registry
11
+ components, and can optionally install the Semaphor Agent Plugin for detected
12
+ Codex and Claude Code installations.
12
13
 
13
14
  It does not authenticate to Semaphor, write tokens, or choose a project/domain.
14
15
  The Semaphor Agent Plugin handles OAuth, project selection, runtime token
@@ -25,17 +26,63 @@ When `app-name` is omitted, the CLI creates `./semaphor-data-app`.
25
26
  Options:
26
27
 
27
28
  ```text
28
- --no-install Skip dependency installation.
29
+ --no-install Skip dependency installation. Requires --components none.
29
30
  --package-manager <name> Use npm, pnpm, yarn, or bun. Defaults to detected npm.
30
31
  --skip-plugin Skip Codex/Claude plugin install prompts.
31
32
  --install-codex-plugin Install the Codex plugin without prompting.
32
33
  --install-claude-plugin Install the Claude Code plugin without prompting.
33
34
  --template <source> Starter source. Defaults to the public starter repo.
34
35
  --template-ref <ref> Git branch/tag for the default starter repo. Defaults to main.
36
+ --shadcn-preset <preset> Apply a shadcn preset before adding Semaphor components.
37
+ --shadcn-base <base> Pass base or radix to shadcn init when using a preset.
38
+ --components <list> Add Semaphor registry components. Defaults to all. Use none, query, metrics,
39
+ filters, card, table, matrix, recommended, all, or a comma-separated list.
35
40
  --yes Use noninteractive defaults; skip optional plugin installs unless explicit.
36
41
  --help Show help.
37
42
  ```
38
43
 
44
+ ## shadcn Presets And Semaphor Components
45
+
46
+ The starter ships with a working shadcn setup. For teams that bring their own
47
+ style, pass a shadcn preset during creation:
48
+
49
+ ```bash
50
+ npx create-semaphor-app@latest my-app --shadcn-preset <preset-id>
51
+ ```
52
+
53
+ Semaphor UI helpers are added from the public shadcn registry at scaffold time.
54
+ By default, `create-semaphor-app` installs all production app-building
55
+ components. Use `--components none` when you want the starter without Semaphor
56
+ registry components:
57
+
58
+ ```bash
59
+ npx create-semaphor-app@latest my-app
60
+ npx create-semaphor-app@latest my-app --components none
61
+ npx create-semaphor-app@latest my-app --components table
62
+ npx create-semaphor-app@latest my-app --components query-state,view-card,metric-kpis,filter-controls,server-data-table,matrix-table
63
+ ```
64
+
65
+ Component presets:
66
+
67
+ | Value | Installs |
68
+ | --- | --- |
69
+ | `none` | No Semaphor registry components. |
70
+ | `query` | `query-state` |
71
+ | `metrics` | `query-state-boundary`, `metric-kpis` |
72
+ | `filters` | `filter-controls` |
73
+ | `card` | `view-card` |
74
+ | `table` | `query-state`, `server-data-table` |
75
+ | `matrix` | `query-state`, `matrix-table` |
76
+ | `recommended` | `query-state`, `query-state-boundary`, `view-card`, `metric-kpis`, `filter-controls`, `server-data-table` |
77
+ | `all` | `query-state`, `query-state-boundary`, `view-card`, `metric-kpis`, `filter-controls`, `server-data-table`, `matrix-table`. This is the default. |
78
+
79
+ Registry components install as source into the generated app. They are optional
80
+ UI accelerators and do not replace `react-semaphor/data-app-sdk`.
81
+
82
+ `--shadcn-preset` and installed registry components run the shadcn CLI. To skip
83
+ installs, pass `--no-install --components none`, then run the shadcn commands
84
+ manually later if needed.
85
+
39
86
  ## Local Validation
40
87
 
41
88
  ```bash
@@ -13,6 +13,47 @@ const DEFAULT_TEMPLATE_REF = 'main';
13
13
  const DEFAULT_APP_NAME = 'semaphor-data-app';
14
14
  const MARKETPLACE = 'semaphor-analytics/agent-plugin';
15
15
  const PLUGIN_ID = 'semaphor@semaphor-analytics';
16
+ const SEMAPHOR_COMPONENT_REGISTRY = 'semaphor-analytics/semaphor-data-app-components';
17
+ const SEMAPHOR_COMPONENT_PRESETS = Object.freeze({
18
+ none: [],
19
+ query: ['query-state'],
20
+ table: ['query-state', 'server-data-table'],
21
+ metrics: ['query-state-boundary', 'metric-kpis'],
22
+ filters: ['filter-controls'],
23
+ card: ['view-card'],
24
+ matrix: ['query-state', 'matrix-table'],
25
+ recommended: [
26
+ 'query-state',
27
+ 'query-state-boundary',
28
+ 'view-card',
29
+ 'metric-kpis',
30
+ 'filter-controls',
31
+ 'server-data-table',
32
+ ],
33
+ all: [
34
+ 'query-state',
35
+ 'query-state-boundary',
36
+ 'view-card',
37
+ 'metric-kpis',
38
+ 'filter-controls',
39
+ 'server-data-table',
40
+ 'matrix-table',
41
+ ],
42
+ });
43
+ const SEMAPHOR_COMPONENT_ALIASES = Object.freeze({
44
+ 'query-state': 'query-state',
45
+ 'query-state-boundary': 'query-state-boundary',
46
+ 'view-card': 'view-card',
47
+ card: 'view-card',
48
+ 'server-data-table': 'server-data-table',
49
+ table: 'server-data-table',
50
+ 'metric-kpis': 'metric-kpis',
51
+ kpis: 'metric-kpis',
52
+ 'filter-controls': 'filter-controls',
53
+ filters: 'filter-controls',
54
+ 'matrix-table': 'matrix-table',
55
+ matrix: 'matrix-table',
56
+ });
16
57
  const canPrompt = Boolean(process.stdin.isTTY && process.stdout.isTTY);
17
58
 
18
59
  const cwd = process.cwd();
@@ -24,13 +65,17 @@ Usage:
24
65
  npx create-semaphor-app@latest [app-name] [options]
25
66
 
26
67
  Options:
27
- --no-install Skip dependency installation.
68
+ --no-install Skip dependency installation. Requires --components none.
28
69
  --package-manager <name> Use npm, pnpm, yarn, or bun.
29
70
  --skip-plugin Skip Codex/Claude plugin install prompts.
30
71
  --install-codex-plugin Install the Codex plugin without prompting.
31
72
  --install-claude-plugin Install the Claude Code plugin without prompting.
32
73
  --template <source> Starter source. Can be a local directory or git URL.
33
74
  --template-ref <ref> Git branch/tag for the default starter repo.
75
+ --shadcn-preset <preset> Apply a shadcn preset before adding Semaphor components.
76
+ --shadcn-base <base> Pass base or radix to shadcn init when using a preset.
77
+ --components <list> Add Semaphor registry components. Defaults to all. Use none, query, metrics,
78
+ filters, card, table, matrix, recommended, all, or a comma-separated list.
34
79
  --yes, -y Use noninteractive defaults; skip optional plugin installs unless explicit.
35
80
  --help, -h Show this help.
36
81
  `;
@@ -46,6 +91,9 @@ function parseArgs(argv) {
46
91
  installClaudePlugin: false,
47
92
  template: DEFAULT_TEMPLATE_REPO,
48
93
  templateRef: DEFAULT_TEMPLATE_REF,
94
+ shadcnPreset: null,
95
+ shadcnBase: null,
96
+ components: 'all',
49
97
  yes: false,
50
98
  help: false,
51
99
  };
@@ -74,6 +122,18 @@ function parseArgs(argv) {
74
122
  parsed.templateRef = readRequiredValue(argv, ++i, arg);
75
123
  } else if (arg.startsWith('--template-ref=')) {
76
124
  parsed.templateRef = arg.slice('--template-ref='.length);
125
+ } else if (arg === '--shadcn-preset') {
126
+ parsed.shadcnPreset = readRequiredValue(argv, ++i, arg);
127
+ } else if (arg.startsWith('--shadcn-preset=')) {
128
+ parsed.shadcnPreset = arg.slice('--shadcn-preset='.length);
129
+ } else if (arg === '--shadcn-base') {
130
+ parsed.shadcnBase = readRequiredValue(argv, ++i, arg);
131
+ } else if (arg.startsWith('--shadcn-base=')) {
132
+ parsed.shadcnBase = arg.slice('--shadcn-base='.length);
133
+ } else if (arg === '--components') {
134
+ parsed.components = readRequiredValue(argv, ++i, arg);
135
+ } else if (arg.startsWith('--components=')) {
136
+ parsed.components = arg.slice('--components='.length);
77
137
  } else if (arg === '--yes' || arg === '-y') {
78
138
  parsed.yes = true;
79
139
  } else if (arg.startsWith('-')) {
@@ -225,6 +285,23 @@ function resolvePackageManager(requested) {
225
285
  return 'npm';
226
286
  }
227
287
 
288
+ function hasOwn(object, key) {
289
+ return Object.prototype.hasOwnProperty.call(object, key);
290
+ }
291
+
292
+ function resolveShadcnRunner(packageManager) {
293
+ if (packageManager === 'pnpm') {
294
+ return { command: 'pnpm', args: ['dlx', 'shadcn@latest'] };
295
+ }
296
+ if (packageManager === 'yarn') {
297
+ return { command: 'yarn', args: ['dlx', 'shadcn@latest'] };
298
+ }
299
+ if (packageManager === 'bun') {
300
+ return { command: 'bunx', args: ['--bun', 'shadcn@latest'] };
301
+ }
302
+ return { command: 'npx', args: ['shadcn@latest'] };
303
+ }
304
+
228
305
  function installArgs(packageManager) {
229
306
  if (packageManager === 'yarn') return [];
230
307
  return ['install'];
@@ -315,6 +392,107 @@ function loadTemplate(template, templateRef) {
315
392
  return { tempRoot, templateDir };
316
393
  }
317
394
 
395
+ function normalizeSemaphorComponents(value) {
396
+ const raw = String(value || 'none')
397
+ .split(',')
398
+ .map((entry) => entry.trim().toLowerCase())
399
+ .filter(Boolean);
400
+ if (raw.length === 0) return [];
401
+
402
+ const components = [];
403
+ for (const entry of raw) {
404
+ if (hasOwn(SEMAPHOR_COMPONENT_PRESETS, entry)) {
405
+ components.push(...SEMAPHOR_COMPONENT_PRESETS[entry]);
406
+ continue;
407
+ }
408
+ if (!hasOwn(SEMAPHOR_COMPONENT_ALIASES, entry)) {
409
+ throw new Error(
410
+ `Unsupported Semaphor component "${entry}". Use none, query, metrics, filters, card, table, matrix, recommended, all, or one of ${Object.keys(SEMAPHOR_COMPONENT_ALIASES).join(', ')}.`,
411
+ );
412
+ }
413
+ components.push(SEMAPHOR_COMPONENT_ALIASES[entry]);
414
+ }
415
+
416
+ return Array.from(new Set(components));
417
+ }
418
+
419
+ function normalizeShadcnBase(value) {
420
+ if (!value) return null;
421
+ const normalized = String(value).trim().toLowerCase();
422
+ if (!['base', 'radix'].includes(normalized)) {
423
+ throw new Error('--shadcn-base must be base or radix.');
424
+ }
425
+ return normalized;
426
+ }
427
+
428
+ function validateScaffoldOptions(args) {
429
+ const components = normalizeSemaphorComponents(args.components);
430
+ normalizeShadcnBase(args.shadcnBase);
431
+
432
+ if (args.shadcnBase && !args.shadcnPreset) {
433
+ throw new Error('--shadcn-base requires --shadcn-preset.');
434
+ }
435
+
436
+ if (args.shadcnPreset !== null && String(args.shadcnPreset).trim() === '') {
437
+ throw new Error('--shadcn-preset requires a non-empty preset id.');
438
+ }
439
+
440
+ if (!args.install && (args.shadcnPreset || components.length > 0)) {
441
+ throw new Error(
442
+ '--no-install cannot be combined with --shadcn-preset or --components. Run create-semaphor-app with installs enabled, or add shadcn components manually after scaffolding.',
443
+ );
444
+ }
445
+ }
446
+
447
+ function runShadcn(packageManager, targetDir, args) {
448
+ const runner = resolveShadcnRunner(packageManager);
449
+ ensureCommand(runner.command, runner.command);
450
+ runRequired(runner.command, [...runner.args, ...args], {
451
+ cwd: targetDir,
452
+ stdio: 'inherit',
453
+ });
454
+ }
455
+
456
+ function customizeShadcn(args, packageManager, targetDir) {
457
+ const components = normalizeSemaphorComponents(args.components);
458
+ const shadcnBase = normalizeShadcnBase(args.shadcnBase);
459
+
460
+ if (!args.shadcnPreset && components.length === 0) {
461
+ return { presetApplied: false, components };
462
+ }
463
+
464
+ if (args.shadcnPreset) {
465
+ const initArgs = [
466
+ 'init',
467
+ '--preset',
468
+ args.shadcnPreset,
469
+ '--template',
470
+ 'vite',
471
+ '--force',
472
+ '--reinstall',
473
+ ];
474
+ if (shadcnBase) {
475
+ initArgs.push('--base', shadcnBase);
476
+ }
477
+ console.log(`\nApplying shadcn preset ${args.shadcnPreset}...`);
478
+ runShadcn(packageManager, targetDir, initArgs);
479
+ console.log('✓ Applied shadcn preset');
480
+ }
481
+
482
+ if (components.length > 0) {
483
+ console.log('\nAdding Semaphor registry components...');
484
+ for (const component of components) {
485
+ runShadcn(packageManager, targetDir, [
486
+ 'add',
487
+ `${SEMAPHOR_COMPONENT_REGISTRY}/${component}`,
488
+ ]);
489
+ console.log(`✓ Added ${component}`);
490
+ }
491
+ }
492
+
493
+ return { presetApplied: Boolean(args.shadcnPreset), components };
494
+ }
495
+
318
496
  function detectAgents() {
319
497
  return {
320
498
  codex: commandExists('codex'),
@@ -411,12 +589,22 @@ async function maybeInstallPlugins(args, rl) {
411
589
  return { detected, results, skipped: false };
412
590
  }
413
591
 
414
- function printNextSteps({ targetDir, packageManager, pluginSummary }) {
592
+ function printNextSteps({ targetDir, packageManager, pluginSummary, shadcnSummary }) {
415
593
  const relativeTarget = path.relative(cwd, targetDir) || '.';
416
594
  console.log('\nNext:');
417
595
  console.log(` cd ${relativeTarget}`);
418
596
  console.log(` ${packageManager} run dev`);
419
597
 
598
+ if (shadcnSummary?.presetApplied || shadcnSummary?.components?.length > 0) {
599
+ console.log('\nConfigured shadcn:');
600
+ if (shadcnSummary.presetApplied) {
601
+ console.log(' - Applied custom preset');
602
+ }
603
+ for (const component of shadcnSummary.components ?? []) {
604
+ console.log(` - Added Semaphor ${component}`);
605
+ }
606
+ }
607
+
420
608
  if (pluginSummary?.skipped) {
421
609
  printPluginInstructions();
422
610
  printBuildPrompt();
@@ -487,6 +675,14 @@ async function main() {
487
675
  console.log(usage());
488
676
  return;
489
677
  }
678
+ try {
679
+ validateScaffoldOptions(args);
680
+ } catch (error) {
681
+ console.error(error instanceof Error ? error.message : String(error));
682
+ console.error('');
683
+ console.error(usage());
684
+ process.exit(1);
685
+ }
490
686
 
491
687
  const rl = readline.createInterface({
492
688
  input: process.stdin,
@@ -520,11 +716,13 @@ async function main() {
520
716
  console.log('Skipping dependency installation.');
521
717
  }
522
718
 
719
+ const shadcnSummary = customizeShadcn(args, packageManager, target.targetDir);
523
720
  const pluginSummary = await maybeInstallPlugins(args, rl);
524
721
  printNextSteps({
525
722
  targetDir: target.targetDir,
526
723
  packageManager,
527
724
  pluginSummary,
725
+ shadcnSummary,
528
726
  });
529
727
  } catch (error) {
530
728
  console.error('\nUnable to create Semaphor app.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-semaphor-app",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Create a Semaphor Data App starter project.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -98,6 +98,8 @@ async function main() {
98
98
  '--template',
99
99
  defaultLocalStarterPath,
100
100
  '--no-install',
101
+ '--components',
102
+ 'none',
101
103
  '--skip-plugin',
102
104
  '--yes',
103
105
  ], { cwd: root });
@@ -113,6 +115,8 @@ async function main() {
113
115
  '--template',
114
116
  defaultLocalStarterPath,
115
117
  '--no-install',
118
+ '--components',
119
+ 'none',
116
120
  '--skip-plugin',
117
121
  '--yes',
118
122
  ], { cwd: root });
@@ -126,6 +130,8 @@ async function main() {
126
130
  const result = runCli([
127
131
  'github-template-app',
128
132
  '--no-install',
133
+ '--components',
134
+ 'none',
129
135
  '--skip-plugin',
130
136
  '--yes',
131
137
  ], { cwd: root });
@@ -146,6 +152,8 @@ async function main() {
146
152
  '--template',
147
153
  defaultLocalStarterPath,
148
154
  '--no-install',
155
+ '--components',
156
+ 'none',
149
157
  '--install-codex-plugin',
150
158
  ], {
151
159
  cwd: root,
@@ -168,6 +176,146 @@ async function main() {
168
176
  console.log('✓ fake Codex plugin install path');
169
177
  }
170
178
 
179
+ {
180
+ const root = createTempRoot('default-components-');
181
+ const binDir = path.join(root, 'bin');
182
+ fs.mkdirSync(binDir, { recursive: true });
183
+ const shadcnLog = path.join(root, 'shadcn.log');
184
+ const npmLog = path.join(root, 'npm.log');
185
+ createFakeCommand(binDir, 'npx', shadcnLog);
186
+ createFakeCommand(binDir, 'npm', npmLog);
187
+
188
+ const result = runCli([
189
+ 'default-components-app',
190
+ '--template',
191
+ defaultLocalStarterPath,
192
+ '--skip-plugin',
193
+ '--yes',
194
+ '--package-manager',
195
+ 'npm',
196
+ ], {
197
+ cwd: root,
198
+ env: {
199
+ ...process.env,
200
+ PATH: `${binDir}${path.delimiter}${process.env.PATH || ''}`,
201
+ },
202
+ });
203
+ assertSuccess(result, 'default component install');
204
+ assertScaffold(root, 'default-components-app');
205
+ const npmInstallLog = fs.readFileSync(npmLog, 'utf8');
206
+ assert(npmInstallLog.includes('npm install'), 'expected npm install command');
207
+ const log = fs.readFileSync(shadcnLog, 'utf8');
208
+ for (const component of [
209
+ 'query-state',
210
+ 'query-state-boundary',
211
+ 'view-card',
212
+ 'metric-kpis',
213
+ 'filter-controls',
214
+ 'server-data-table',
215
+ 'matrix-table',
216
+ ]) {
217
+ assert(
218
+ log.includes(`npx shadcn@latest add semaphor-analytics/semaphor-data-app-components/${component}`),
219
+ `expected default ${component} registry add command`,
220
+ );
221
+ }
222
+ console.log('✓ default Semaphor registry component install');
223
+ }
224
+
225
+ {
226
+ const root = createTempRoot('shadcn-');
227
+ const binDir = path.join(root, 'bin');
228
+ fs.mkdirSync(binDir, { recursive: true });
229
+ const shadcnLog = path.join(root, 'shadcn.log');
230
+ const npmLog = path.join(root, 'npm.log');
231
+ createFakeCommand(binDir, 'npx', shadcnLog);
232
+ createFakeCommand(binDir, 'npm', npmLog);
233
+
234
+ const result = runCli([
235
+ 'styled-app',
236
+ '--template',
237
+ defaultLocalStarterPath,
238
+ '--skip-plugin',
239
+ '--yes',
240
+ '--package-manager',
241
+ 'npm',
242
+ '--shadcn-preset',
243
+ 'test-preset',
244
+ '--shadcn-base',
245
+ 'base',
246
+ '--components',
247
+ 'recommended,matrix-table',
248
+ ], {
249
+ cwd: root,
250
+ env: {
251
+ ...process.env,
252
+ PATH: `${binDir}${path.delimiter}${process.env.PATH || ''}`,
253
+ },
254
+ });
255
+ assertSuccess(result, 'shadcn preset and component install');
256
+ assertScaffold(root, 'styled-app');
257
+ const npmInstallLog = fs.readFileSync(npmLog, 'utf8');
258
+ assert(npmInstallLog.includes('npm install'), 'expected npm install command');
259
+ const log = fs.readFileSync(shadcnLog, 'utf8');
260
+ assert(
261
+ log.includes('npx shadcn@latest init --preset test-preset --template vite --force --reinstall --base base'),
262
+ 'expected shadcn preset init command',
263
+ );
264
+ assert(
265
+ log.includes('npx shadcn@latest add semaphor-analytics/semaphor-data-app-components/query-state'),
266
+ 'expected query-state registry add command',
267
+ );
268
+ assert(
269
+ log.includes('npx shadcn@latest add semaphor-analytics/semaphor-data-app-components/query-state-boundary'),
270
+ 'expected query-state-boundary registry add command',
271
+ );
272
+ assert(
273
+ log.includes('npx shadcn@latest add semaphor-analytics/semaphor-data-app-components/view-card'),
274
+ 'expected view-card registry add command',
275
+ );
276
+ assert(
277
+ log.includes('npx shadcn@latest add semaphor-analytics/semaphor-data-app-components/metric-kpis'),
278
+ 'expected metric-kpis registry add command',
279
+ );
280
+ assert(
281
+ log.includes('npx shadcn@latest add semaphor-analytics/semaphor-data-app-components/filter-controls'),
282
+ 'expected filter-controls registry add command',
283
+ );
284
+ assert(
285
+ log.includes('npx shadcn@latest add semaphor-analytics/semaphor-data-app-components/server-data-table'),
286
+ 'expected server-data-table registry add command',
287
+ );
288
+ assert(
289
+ log.includes('npx shadcn@latest add semaphor-analytics/semaphor-data-app-components/matrix-table'),
290
+ 'expected matrix-table registry add command',
291
+ );
292
+ console.log('✓ shadcn preset and registry component path');
293
+ }
294
+
295
+ {
296
+ const root = createTempRoot('no-install-shadcn-');
297
+ const result = runCli([
298
+ 'invalid-shadcn-app',
299
+ '--template',
300
+ defaultLocalStarterPath,
301
+ '--no-install',
302
+ '--skip-plugin',
303
+ '--yes',
304
+ '--components',
305
+ 'table',
306
+ ], { cwd: root });
307
+ assert(result.status !== 0, 'expected shadcn options with --no-install to fail');
308
+ assert(
309
+ result.stderr.includes('--no-install cannot be combined'),
310
+ 'expected actionable --no-install error',
311
+ );
312
+ assert(
313
+ !fs.existsSync(path.join(root, 'invalid-shadcn-app')),
314
+ 'invalid shadcn options should fail before scaffolding',
315
+ );
316
+ console.log('✓ rejects shadcn customization with --no-install');
317
+ }
318
+
171
319
  console.log('✓ cleaned up temp projects');
172
320
  } finally {
173
321
  for (const root of tempRoots.reverse()) {