@skilly-hand/skilly-hand 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.
@@ -9,11 +9,22 @@ import {
9
9
  installProject,
10
10
  resolveSkillSelection,
11
11
  runDoctor,
12
+ setupNativeProject,
12
13
  uninstallProject
13
14
  } from "../../core/src/index.js";
14
15
  import { createTerminalRenderer } from "../../core/src/terminal.js";
15
16
  import { detectProject } from "../../detectors/src/index.js";
16
17
  import { confirmWithInk, launchInkApp } from "./ink-ui.js";
18
+ import {
19
+ createResultDoc,
20
+ kvBlock,
21
+ listBlock,
22
+ renderResultDocText,
23
+ section,
24
+ statusBlock,
25
+ tableBlock,
26
+ textBlock
27
+ } from "./result-doc.js";
17
28
 
18
29
  const require = createRequire(import.meta.url);
19
30
  const { version } = require("../../../package.json");
@@ -75,13 +86,14 @@ export function parseArgs(argv) {
75
86
  else throw new Error(`Unknown flag: ${token}`);
76
87
  }
77
88
 
78
- return { command: positional[0], flags };
89
+ return { command: positional[0], subcommand: positional[1], flags };
79
90
  }
80
91
 
81
92
  function buildHelpText(renderer, appVersion) {
82
93
  const usage = renderer.section("Usage", renderer.list([
83
94
  "npx skilly-hand # interactive launcher when running in a TTY",
84
95
  "npx skilly-hand install",
96
+ "npx skilly-hand native setup",
85
97
  "npx skilly-hand detect",
86
98
  "npx skilly-hand list",
87
99
  "npx skilly-hand doctor",
@@ -104,6 +116,7 @@ function buildHelpText(renderer, appVersion) {
104
116
  const examples = renderer.section("Examples", renderer.list([
105
117
  "npx skilly-hand",
106
118
  "npx skilly-hand install --dry-run",
119
+ "npx skilly-hand native setup --agent codex",
107
120
  "npx skilly-hand detect --json",
108
121
  "npx skilly-hand install --agent antigravity --agent windsurf",
109
122
  "npx skilly-hand uninstall --yes"
@@ -117,151 +130,216 @@ function buildHelpText(renderer, appVersion) {
117
130
  ]);
118
131
  }
119
132
 
120
- function buildInstallResultBlock(renderer, appVersion, result, flags) {
133
+ function buildInstallResultDoc(result, flags, detectionGridText = "") {
121
134
  const mode = flags.dryRun ? "dry-run" : "apply";
122
- const preflight = renderer.section(
123
- "Install Preflight",
124
- renderer.kv([
125
- ["Project", result.plan.cwd],
126
- ["Install root", result.plan.installRoot],
127
- ["Agents", result.plan.agents.join(", ") || "none"],
128
- ["Include tags", flags.include.join(", ") || "none"],
129
- ["Exclude tags", flags.exclude.join(", ") || "none"],
130
- ["Mode", mode]
135
+ return createResultDoc("Install", [
136
+ section("Install Preflight", [
137
+ kvBlock([
138
+ ["Project", result.plan.cwd],
139
+ ["Install root", result.plan.installRoot],
140
+ ["Agents", result.plan.agents.join(", ") || "none"],
141
+ ["Include tags", flags.include.join(", ") || "none"],
142
+ ["Exclude tags", flags.exclude.join(", ") || "none"],
143
+ ["Mode", mode]
144
+ ])
145
+ ]),
146
+ section("Detected Technologies", [
147
+ result.plan.detections.length > 0
148
+ ? textBlock(detectionGridText)
149
+ : statusBlock("warn", "No technology signals were detected.", "Only core skills will be selected.")
150
+ ]),
151
+ section("Skill Plan", [
152
+ result.plan.skills.length > 0
153
+ ? tableBlock(
154
+ [
155
+ { key: "id", header: "Skill ID" },
156
+ { key: "title", header: "Title" },
157
+ { key: "tags", header: "Tags" }
158
+ ],
159
+ result.plan.skills.map((skill) => ({
160
+ id: skill.id,
161
+ title: skill.title,
162
+ tags: skill.tags.join(", ")
163
+ }))
164
+ )
165
+ : statusBlock("warn", "No skills selected.")
166
+ ]),
167
+ section("Status", [
168
+ result.applied
169
+ ? statusBlock("success", "Installation completed.", "Managed files and symlinks are in place.")
170
+ : statusBlock("info", "Dry run complete.", "No files were written.")
171
+ ]),
172
+ section("Next Steps", [
173
+ listBlock(
174
+ result.applied
175
+ ? [
176
+ "Review generated AGENTS and assistant instruction files.",
177
+ "Run `npx skilly-hand native setup` to scaffold native instruction/rule adapters.",
178
+ "Run `npx skilly-hand doctor` to validate installation health.",
179
+ "Use `npx skilly-hand uninstall` to restore backed-up files if needed."
180
+ ]
181
+ : [
182
+ "Run `npx skilly-hand install` to apply this plan.",
183
+ "Adjust `--include` and `--exclude` tags to tune skill selection."
184
+ ]
185
+ )
131
186
  ])
132
- );
187
+ ]);
188
+ }
133
189
 
134
- const detections = renderer.section(
135
- "Detected Technologies",
136
- result.plan.detections.length > 0
137
- ? renderer.detectionGrid(result.plan.detections)
138
- : renderer.status("warn", "No technology signals were detected.", "Only core skills will be selected.")
139
- );
190
+ function buildNativeSetupResultDoc(result, flags) {
191
+ const mode = flags.dryRun ? "dry-run" : "apply";
192
+ return createResultDoc("Native Setup", [
193
+ section("Native Setup Preflight", [
194
+ kvBlock([
195
+ ["Project", result.plan.cwd],
196
+ ["Install root", result.plan.installRoot],
197
+ ["Agents", result.plan.agents.join(", ") || "none"],
198
+ ["Mode", mode]
199
+ ])
200
+ ]),
201
+ section("Native Coverage", [
202
+ tableBlock(
203
+ [
204
+ { key: "agent", header: "Agent" },
205
+ { key: "status", header: "Status" },
206
+ { key: "target", header: "Target" },
207
+ { key: "remediation", header: "Remediation" }
208
+ ],
209
+ (result.nativeStatus || result.plan.nativeStatus || []).map((row) => ({
210
+ agent: row.agent,
211
+ status: row.status,
212
+ target: row.target || "-",
213
+ remediation: row.remediation
214
+ }))
215
+ )
216
+ ]),
217
+ section("Status", [
218
+ result.applied
219
+ ? statusBlock("success", "Native setup completed.", "Native rule/instruction files are synchronized.")
220
+ : statusBlock("info", "Native setup dry run complete.", "No files were written.")
221
+ ]),
222
+ section("Next Steps", [
223
+ listBlock(
224
+ result.applied
225
+ ? [
226
+ "Run `npx skilly-hand doctor` to verify native coverage.",
227
+ "Re-run `npx skilly-hand native setup` after changing agent targets."
228
+ ]
229
+ : [
230
+ "Run `npx skilly-hand native setup` to apply these native adapter changes."
231
+ ]
232
+ )
233
+ ])
234
+ ]);
235
+ }
140
236
 
141
- const skills = renderer.section(
142
- "Skill Plan",
143
- result.plan.skills.length > 0
144
- ? renderer.table(
145
- [
146
- { key: "id", header: "Skill ID" },
147
- { key: "title", header: "Title" },
148
- { key: "tags", header: "Tags" }
149
- ],
150
- result.plan.skills.map((skill) => ({
151
- id: skill.id,
152
- title: skill.title,
153
- tags: skill.tags.join(", ")
154
- }))
155
- )
156
- : renderer.status("warn", "No skills selected.")
157
- );
237
+ function renderResultDoc(renderer, appVersion, doc, options = {}) {
238
+ return renderResultDocText(renderer, appVersion, doc, options);
239
+ }
158
240
 
159
- const status = result.applied
160
- ? renderer.status("success", "Installation completed.", "Managed files and symlinks are in place.")
161
- : renderer.status("info", "Dry run complete.", "No files were written.");
241
+ function buildInstallResultBlock(renderer, appVersion, result, flags, options = {}) {
242
+ const doc = buildInstallResultDoc(result, flags, renderer.detectionGrid(result.plan.detections));
243
+ return renderResultDoc(renderer, appVersion, doc, options);
244
+ }
162
245
 
163
- const nextSteps = result.applied
164
- ? renderer.nextSteps([
165
- "Review generated AGENTS and assistant instruction files.",
166
- "Run `npx skilly-hand doctor` to validate installation health.",
167
- "Use `npx skilly-hand uninstall` to restore backed-up files if needed."
168
- ])
169
- : renderer.nextSteps([
170
- "Run `npx skilly-hand install` to apply this plan.",
171
- "Adjust `--include` and `--exclude` tags to tune skill selection."
172
- ]);
246
+ function buildNativeSetupResultBlock(renderer, appVersion, result, flags, options = {}) {
247
+ return renderResultDoc(renderer, appVersion, buildNativeSetupResultDoc(result, flags), options);
248
+ }
173
249
 
174
- return renderer.joinBlocks([renderer.banner(appVersion), preflight, detections, skills, status, nextSteps]);
250
+ function printNativeSetupResult(renderer, appVersion, result, flags) {
251
+ renderer.write(buildNativeSetupResultBlock(renderer, appVersion, result, flags, { includeBanner: true }));
175
252
  }
176
253
 
177
254
  function printInstallResult(renderer, appVersion, result, flags) {
178
- renderer.write(buildInstallResultBlock(renderer, appVersion, result, flags));
255
+ renderer.write(buildInstallResultBlock(renderer, appVersion, result, flags, { includeBanner: true }));
179
256
  }
180
257
 
181
- function buildDetectResultBlock(renderer, cwd, detections) {
182
- const summary = renderer.section(
183
- "Detection Summary",
184
- renderer.kv([
185
- ["Project", cwd],
186
- ["Signals found", String(detections.length)]
258
+ function buildDetectResultDoc(cwd, detections, detectionGridText = "") {
259
+ return createResultDoc("Detect", [
260
+ section("Detection Summary", [
261
+ kvBlock([
262
+ ["Project", cwd],
263
+ ["Signals found", String(detections.length)]
264
+ ])
265
+ ]),
266
+ section("Findings", [
267
+ detections.length > 0
268
+ ? textBlock(detectionGridText)
269
+ : statusBlock("warn", "No technology signals were detected.", "Only core skills will be selected.")
187
270
  ])
188
- );
189
-
190
- const findings = renderer.section(
191
- "Findings",
192
- detections.length > 0
193
- ? renderer.detectionGrid(detections)
194
- : renderer.status("warn", "No technology signals were detected.", "Only core skills will be selected.")
195
- );
271
+ ]);
272
+ }
196
273
 
197
- return renderer.joinBlocks([summary, findings]);
274
+ function buildDetectResultBlock(renderer, cwd, detections) {
275
+ const doc = buildDetectResultDoc(cwd, detections, renderer.detectionGrid(detections));
276
+ return renderResultDoc(renderer, "", doc, { includeBanner: false });
198
277
  }
199
278
 
200
279
  function printDetectResult(renderer, cwd, detections) {
201
280
  renderer.write(buildDetectResultBlock(renderer, cwd, detections));
202
281
  }
203
282
 
204
- function buildListResultBlock(renderer, skills) {
205
- const summary = renderer.section(
206
- "Catalog Summary",
207
- renderer.kv([["Skills available", String(skills.length)]])
208
- );
209
-
210
- const table = renderer.section(
211
- "Skills",
212
- renderer.table(
213
- [
214
- { key: "id", header: "Skill ID" },
215
- { key: "title", header: "Title" },
216
- { key: "tags", header: "Tags" },
217
- { key: "agents", header: "Agents" }
218
- ],
219
- skills.map((skill) => ({
220
- id: skill.id,
221
- title: skill.title,
222
- tags: skill.tags.join(", "),
223
- agents: skill.agentSupport.join(", ")
224
- }))
225
- )
226
- );
283
+ function buildListResultDoc(skills) {
284
+ return createResultDoc("List", [
285
+ section("Catalog Summary", [kvBlock([["Skills available", String(skills.length)]])]),
286
+ section("Skills", [
287
+ tableBlock(
288
+ [
289
+ { key: "id", header: "Skill ID" },
290
+ { key: "title", header: "Title" },
291
+ { key: "tags", header: "Tags" },
292
+ { key: "agents", header: "Agents" }
293
+ ],
294
+ skills.map((skill) => ({
295
+ id: skill.id,
296
+ title: skill.title,
297
+ tags: skill.tags.join(", "),
298
+ agents: skill.agentSupport.join(", ")
299
+ }))
300
+ )
301
+ ])
302
+ ]);
303
+ }
227
304
 
228
- return renderer.joinBlocks([summary, table]);
305
+ function buildListResultBlock(renderer, skills) {
306
+ return renderResultDoc(renderer, "", buildListResultDoc(skills), { includeBanner: false });
229
307
  }
230
308
 
231
309
  function printListResult(renderer, skills) {
232
310
  renderer.write(buildListResultBlock(renderer, skills));
233
311
  }
234
312
 
235
- function buildDoctorResultBlock(renderer, result) {
236
- const badge = renderer.healthBadge(result.installed);
237
-
238
- const summary = renderer.section(
239
- "Doctor Summary",
240
- renderer.kv([
241
- ["Project", result.cwd],
242
- ["Installed", result.installed ? "yes" : "no"],
243
- ["Catalog issues", String(result.catalogIssues.length)]
313
+ function buildDoctorResultDoc(result, healthBadgeText = "") {
314
+ const sections = [
315
+ section("Health", [textBlock(healthBadgeText)]),
316
+ section("Doctor Summary", [
317
+ kvBlock([
318
+ ["Project", result.cwd],
319
+ ["Installed", result.installed ? "yes" : "no"],
320
+ ["Catalog issues", String(result.catalogIssues.length)]
321
+ ])
244
322
  ])
245
- );
246
-
247
- const lock = result.lock
248
- ? renderer.section(
249
- "Lock Metadata",
250
- renderer.kv([
251
- ["Generated at", result.lock.generatedAt],
252
- ["Agents", result.lock.agents.join(", ")],
253
- ["Skills", result.lock.skills.join(", ")]
254
- ])
255
- )
256
- : "";
323
+ ];
324
+
325
+ if (result.lock) {
326
+ sections.push(section("Lock Metadata", [
327
+ kvBlock([
328
+ ["Generated at", result.lock.generatedAt],
329
+ ["Agents", result.lock.agents.join(", ")],
330
+ ["Skills", result.lock.skills.join(", ")]
331
+ ])
332
+ ]));
333
+ }
257
334
 
258
- const issues = result.catalogIssues.length
259
- ? renderer.section("Catalog Issues", renderer.list(result.catalogIssues))
260
- : renderer.section("Catalog Issues", renderer.status("success", "No catalog issues found."));
335
+ sections.push(section("Catalog Issues", [
336
+ result.catalogIssues.length
337
+ ? listBlock(result.catalogIssues)
338
+ : statusBlock("success", "No catalog issues found.")
339
+ ]));
261
340
 
262
- const probes = renderer.section(
263
- "Project Probes",
264
- renderer.table(
341
+ sections.push(section("Project Probes", [
342
+ tableBlock(
265
343
  [
266
344
  { key: "path", header: "Path" },
267
345
  { key: "exists", header: "Exists" },
@@ -273,32 +351,60 @@ function buildDoctorResultBlock(renderer, result) {
273
351
  type: item.type || "-"
274
352
  }))
275
353
  )
276
- );
354
+ ]));
277
355
 
278
- return renderer.joinBlocks([badge, summary, lock, issues, probes]);
356
+ sections.push(section("Native Coverage", [
357
+ tableBlock(
358
+ [
359
+ { key: "agent", header: "Agent" },
360
+ { key: "status", header: "Status" },
361
+ { key: "target", header: "Target" },
362
+ { key: "remediation", header: "Remediation" }
363
+ ],
364
+ (result.nativeStatus || []).map((row) => ({
365
+ agent: row.agent,
366
+ status: row.status,
367
+ target: row.target || "-",
368
+ remediation: row.remediation
369
+ }))
370
+ )
371
+ ]));
372
+
373
+ return createResultDoc("Doctor", sections);
374
+ }
375
+
376
+ function buildDoctorResultBlock(renderer, result) {
377
+ const doc = buildDoctorResultDoc(result, renderer.healthBadge(result.installed));
378
+ return renderResultDoc(renderer, "", doc, { includeBanner: false });
279
379
  }
280
380
 
281
381
  function printDoctorResult(renderer, result) {
282
382
  renderer.write(buildDoctorResultBlock(renderer, result));
283
383
  }
284
384
 
285
- function buildUninstallResultBlock(renderer, result) {
385
+ function buildUninstallResultDoc(result) {
286
386
  if (result.removed) {
287
- return renderer.joinBlocks([
288
- renderer.status("success", "skilly-hand installation removed."),
289
- renderer.nextSteps([
290
- "Run `npx skilly-hand install` if you want to reinstall managed files.",
291
- "Run `npx skilly-hand doctor` to confirm the project state."
387
+ return createResultDoc("Uninstall", [
388
+ section("Status", [statusBlock("success", "skilly-hand installation removed.")]),
389
+ section("Next Steps", [
390
+ listBlock([
391
+ "Run `npx skilly-hand install` if you want to reinstall managed files.",
392
+ "Run `npx skilly-hand doctor` to confirm the project state."
393
+ ])
292
394
  ])
293
395
  ]);
294
396
  }
295
397
 
296
- return renderer.joinBlocks([
297
- renderer.status("warn", "Nothing to uninstall.", result.reason),
298
- renderer.nextSteps(["Run `npx skilly-hand install` to create a managed installation first."])
398
+ return createResultDoc("Uninstall", [
399
+ section("Status", [statusBlock("warn", "Nothing to uninstall.", result.reason)]),
400
+ section("Next Steps", [listBlock(["Run `npx skilly-hand install` to create a managed installation first."])])
299
401
  ]);
300
402
  }
301
403
 
404
+ function buildUninstallResultBlock(renderer, result) {
405
+ return renderResultDoc(renderer, "", buildUninstallResultDoc(result), { includeBanner: false });
406
+ }
407
+
302
408
  function printUninstallResult(renderer, result) {
303
409
  renderer.write(buildUninstallResultBlock(renderer, result));
304
410
  }
@@ -307,6 +413,9 @@ export function buildErrorHint(message) {
307
413
  if (message.startsWith("Unknown command:")) {
308
414
  return "Run `npx skilly-hand --help` to see available commands.";
309
415
  }
416
+ if (message.startsWith("Unknown native subcommand:")) {
417
+ return "Use `npx skilly-hand native setup`.";
418
+ }
310
419
  if (message.startsWith("Unknown flag:") || message.startsWith("Missing value")) {
311
420
  return "Check command flags with `npx skilly-hand --help`.";
312
421
  }
@@ -319,6 +428,7 @@ function createServices(overrides = {}) {
319
428
  installProject,
320
429
  resolveSkillSelection,
321
430
  runDoctor,
431
+ setupNativeProject,
322
432
  uninstallProject,
323
433
  detectProject,
324
434
  defaultAgents: DEFAULT_AGENTS,
@@ -367,53 +477,113 @@ async function runInteractiveSession({
367
477
  await interactiveUi.launch({
368
478
  appVersion,
369
479
  actions: {
370
- async runCommand(command) {
480
+ async runCommandBundle(command) {
481
+ if (command === "native-setup") {
482
+ const result = await services.setupNativeProject({ cwd, dryRun: false });
483
+ const doc = buildNativeSetupResultDoc(result, { dryRun: false });
484
+ return {
485
+ doc,
486
+ text: renderResultDoc(renderer, appVersion, doc, { includeBanner: false })
487
+ };
488
+ }
371
489
  if (command === "detect") {
372
490
  const detections = await services.detectProject(cwd);
373
- return buildDetectResultBlock(renderer, cwd, detections);
491
+ const doc = buildDetectResultDoc(cwd, detections, renderer.detectionGrid(detections));
492
+ return {
493
+ doc,
494
+ text: renderResultDoc(renderer, "", doc, { includeBanner: false })
495
+ };
374
496
  }
375
497
  if (command === "list") {
376
498
  const skills = await services.loadAllSkills();
377
- return buildListResultBlock(renderer, skills);
499
+ const doc = buildListResultDoc(skills);
500
+ return {
501
+ doc,
502
+ text: renderResultDoc(renderer, "", doc, { includeBanner: false })
503
+ };
378
504
  }
379
505
  if (command === "doctor") {
380
506
  const result = await services.runDoctor(cwd);
381
- return buildDoctorResultBlock(renderer, result);
507
+ const doc = buildDoctorResultDoc(result, renderer.healthBadge(result.installed));
508
+ return {
509
+ doc,
510
+ text: renderResultDoc(renderer, "", doc, { includeBanner: false })
511
+ };
382
512
  }
383
513
  if (command === "uninstall") {
384
514
  const result = await services.uninstallProject(cwd);
385
- return buildUninstallResultBlock(renderer, result);
515
+ const doc = buildUninstallResultDoc(result);
516
+ return {
517
+ doc,
518
+ text: renderResultDoc(renderer, "", doc, { includeBanner: false })
519
+ };
386
520
  }
387
- return renderer.status("warn", `Unknown command: ${command}`);
521
+ const doc = createResultDoc("Result", [section("Status", [statusBlock("warn", `Unknown command: ${command}`)])]);
522
+ return {
523
+ doc,
524
+ text: renderResultDoc(renderer, "", doc, { includeBanner: false })
525
+ };
526
+ },
527
+ async runCommandDoc(command) {
528
+ const bundle = await this.runCommandBundle(command);
529
+ return bundle.doc;
530
+ },
531
+ async runCommand(command) {
532
+ const bundle = await this.runCommandBundle(command);
533
+ return bundle.text;
388
534
  },
389
535
  async prepareInstall() {
390
536
  return getInteractiveInstallContext({ cwd, services });
391
537
  },
392
- async previewInstall({ selectedSkillIds, selectedAgents }) {
538
+ async previewInstallBundle({ selectedSkillIds, selectedAgents }) {
393
539
  const preview = await services.installProject({
394
540
  cwd,
395
541
  agents: selectedAgents,
396
542
  dryRun: true,
397
543
  selectedSkillIds
398
544
  });
399
- return buildInstallResultBlock(renderer, appVersion, preview, {
545
+ const doc = buildInstallResultDoc(preview, {
400
546
  dryRun: true,
401
547
  include: [],
402
548
  exclude: []
403
- });
549
+ }, renderer.detectionGrid(preview.plan.detections));
550
+ return {
551
+ doc,
552
+ text: renderResultDoc(renderer, appVersion, doc, { includeBanner: false })
553
+ };
404
554
  },
405
- async applyInstall({ selectedSkillIds, selectedAgents }) {
555
+ async previewInstall({ selectedSkillIds, selectedAgents }) {
556
+ const bundle = await this.previewInstallBundle({ selectedSkillIds, selectedAgents });
557
+ return bundle.text;
558
+ },
559
+ async previewInstallDoc({ selectedSkillIds, selectedAgents }) {
560
+ const bundle = await this.previewInstallBundle({ selectedSkillIds, selectedAgents });
561
+ return bundle.doc;
562
+ },
563
+ async applyInstallBundle({ selectedSkillIds, selectedAgents }) {
406
564
  const applied = await services.installProject({
407
565
  cwd,
408
566
  agents: selectedAgents,
409
567
  dryRun: false,
410
568
  selectedSkillIds
411
569
  });
412
- return buildInstallResultBlock(renderer, appVersion, applied, {
570
+ const doc = buildInstallResultDoc(applied, {
413
571
  dryRun: false,
414
572
  include: [],
415
573
  exclude: []
416
- });
574
+ }, renderer.detectionGrid(applied.plan.detections));
575
+ return {
576
+ doc,
577
+ text: renderResultDoc(renderer, appVersion, doc, { includeBanner: false })
578
+ };
579
+ },
580
+ async applyInstall({ selectedSkillIds, selectedAgents }) {
581
+ const bundle = await this.applyInstallBundle({ selectedSkillIds, selectedAgents });
582
+ return bundle.text;
583
+ },
584
+ async applyInstallDoc({ selectedSkillIds, selectedAgents }) {
585
+ const bundle = await this.applyInstallBundle({ selectedSkillIds, selectedAgents });
586
+ return bundle.doc;
417
587
  }
418
588
  }
419
589
  });
@@ -421,6 +591,7 @@ async function runInteractiveSession({
421
591
 
422
592
  async function runCommand({
423
593
  command,
594
+ subcommand,
424
595
  flags,
425
596
  cwd,
426
597
  stdin,
@@ -530,6 +701,32 @@ async function runCommand({
530
701
  return;
531
702
  }
532
703
 
704
+ if (command === "native") {
705
+ if (subcommand && subcommand !== "setup") {
706
+ throw new Error(`Unknown native subcommand: ${subcommand}`);
707
+ }
708
+
709
+ const result = await services.setupNativeProject({
710
+ cwd,
711
+ agents: flags.agents,
712
+ dryRun: flags.dryRun
713
+ });
714
+
715
+ if (flags.json) {
716
+ renderer.writeJson({
717
+ command: "native setup",
718
+ applied: result.applied,
719
+ plan: result.plan,
720
+ nativeStatus: result.nativeStatus || result.plan.nativeStatus || [],
721
+ lockPath: result.lockPath || null
722
+ });
723
+ return;
724
+ }
725
+
726
+ printNativeSetupResult(renderer, appVersion, result, flags);
727
+ return;
728
+ }
729
+
533
730
  throw new Error(`Unknown command: ${command}`);
534
731
  }
535
732
 
@@ -550,7 +747,7 @@ export async function runCli({
550
747
  } = {}) {
551
748
  const renderer = createTerminalRenderer({ stdout, stderr, env, platform });
552
749
  const services = createServices(providedServices);
553
- const { command, flags } = parseArgs(argv);
750
+ const { command, subcommand, flags } = parseArgs(argv);
554
751
 
555
752
  if (flags.help) {
556
753
  if (flags.json) {
@@ -560,6 +757,7 @@ export async function runCli({
560
757
  usage: [
561
758
  "npx skilly-hand",
562
759
  "npx skilly-hand install",
760
+ "npx skilly-hand native setup",
563
761
  "npx skilly-hand detect",
564
762
  "npx skilly-hand list",
565
763
  "npx skilly-hand doctor",
@@ -597,6 +795,7 @@ export async function runCli({
597
795
  const effectiveCommand = command || "install";
598
796
  await runCommand({
599
797
  command: effectiveCommand,
798
+ subcommand,
600
799
  flags,
601
800
  cwd,
602
801
  stdin,