hubbits 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,4316 @@
1
+ import { defineCommand, runMain } from 'citty';
2
+ import * as p5 from '@clack/prompts';
3
+ import pc5 from 'picocolors';
4
+ import open from 'open';
5
+ import { ofetch, FetchError } from 'ofetch';
6
+ import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, createWriteStream, createReadStream, unlinkSync } from 'fs';
7
+ import { homedir } from 'os';
8
+ import { join, resolve, basename, relative } from 'path';
9
+ import ora from 'ora';
10
+ import yaml from 'js-yaml';
11
+ import { mkdir, readdir, stat } from 'fs/promises';
12
+ import { z } from 'zod';
13
+ import { pipeline } from 'stream/promises';
14
+ import { createGunzip, createGzip } from 'zlib';
15
+ import { extract as extract$1, pack as pack$1 } from 'tar-stream';
16
+
17
+ // src/index.ts
18
+ var HUBBITS_DIR = join(homedir(), ".hubbits");
19
+ var CONFIG_PATH = join(HUBBITS_DIR, "config.json");
20
+ var CREDENTIALS_PATH = join(HUBBITS_DIR, "credentials.json");
21
+ var DEFAULT_REGISTRY_URL = "https://api.hubbits.dev";
22
+ function ensureDir() {
23
+ if (!existsSync(HUBBITS_DIR)) {
24
+ mkdirSync(HUBBITS_DIR, { recursive: true });
25
+ }
26
+ }
27
+ function readJsonFile(path) {
28
+ try {
29
+ if (!existsSync(path)) return null;
30
+ const raw = readFileSync(path, "utf-8");
31
+ return JSON.parse(raw);
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+ function writeJsonFile(path, data) {
37
+ ensureDir();
38
+ writeFileSync(path, JSON.stringify(data, null, 2) + "\n", { mode: 384 });
39
+ }
40
+ var DEFAULT_CONFIG = {
41
+ registry_url: DEFAULT_REGISTRY_URL
42
+ };
43
+ function readConfig() {
44
+ const stored = readJsonFile(CONFIG_PATH);
45
+ return { ...DEFAULT_CONFIG, ...stored };
46
+ }
47
+ function readCredentials() {
48
+ return readJsonFile(CREDENTIALS_PATH);
49
+ }
50
+ function writeCredentials(credentials) {
51
+ writeJsonFile(CREDENTIALS_PATH, credentials);
52
+ }
53
+ function deleteCredentials() {
54
+ try {
55
+ if (existsSync(CREDENTIALS_PATH)) {
56
+ unlinkSync(CREDENTIALS_PATH);
57
+ }
58
+ } catch {
59
+ }
60
+ }
61
+ ({
62
+ /** ~/.hubbits/cache/ */
63
+ cacheDir: join(HUBBITS_DIR, "cache"),
64
+ /** ~/.hubbits/progress/ */
65
+ progressDir: join(HUBBITS_DIR, "progress")
66
+ });
67
+
68
+ // src/lib/auth.ts
69
+ var TOKEN_ENV_VAR = "HUBBITS_TOKEN";
70
+ function getToken() {
71
+ const envToken = process.env[TOKEN_ENV_VAR];
72
+ if (envToken) return envToken;
73
+ const creds = readCredentials();
74
+ if (!creds) return null;
75
+ return creds.token;
76
+ }
77
+ function saveToken(credentials) {
78
+ writeCredentials(credentials);
79
+ }
80
+ function clearToken() {
81
+ deleteCredentials();
82
+ }
83
+ function getAuthState() {
84
+ const envToken = process.env[TOKEN_ENV_VAR];
85
+ if (envToken) {
86
+ return { token: envToken };
87
+ }
88
+ const creds = readCredentials();
89
+ if (!creds) return null;
90
+ if (creds.expires_at) {
91
+ const expiresAt = new Date(creds.expires_at);
92
+ if (Date.now() > expiresAt.getTime() - 6e4) {
93
+ return null;
94
+ }
95
+ }
96
+ return creds;
97
+ }
98
+ async function pollDeviceFlow(deviceCode, pollFn, options) {
99
+ const { interval, expiresIn, onTick } = options;
100
+ const startTime = Date.now();
101
+ const timeoutMs = expiresIn * 1e3;
102
+ const intervalMs = interval * 1e3;
103
+ while (Date.now() - startTime < timeoutMs) {
104
+ await sleep(intervalMs);
105
+ onTick?.();
106
+ const result = await pollFn(deviceCode);
107
+ if (result.status === "success") {
108
+ saveToken({
109
+ token: result.token,
110
+ expires_at: result.expires_at,
111
+ username: result.username,
112
+ email: result.email
113
+ });
114
+ return result;
115
+ }
116
+ if (result.status === "expired" || result.status === "error") {
117
+ return result;
118
+ }
119
+ }
120
+ return { status: "expired" };
121
+ }
122
+ function sleep(ms) {
123
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
124
+ }
125
+
126
+ // src/lib/api.ts
127
+ var ApiError = class extends Error {
128
+ statusCode;
129
+ code;
130
+ details;
131
+ constructor(statusCode, code, message, details) {
132
+ super(message);
133
+ this.name = "ApiError";
134
+ this.statusCode = statusCode;
135
+ this.code = code;
136
+ this.details = details;
137
+ }
138
+ };
139
+ function createApiClient(options = {}) {
140
+ const getBaseURL = () => options.baseURL ?? readConfig().registry_url;
141
+ async function request(path, fetchOptions = {}) {
142
+ const baseURL = getBaseURL();
143
+ const token = options.token ?? getToken();
144
+ const incomingHeaders = fetchOptions.headers ?? {};
145
+ const headers = {
146
+ "User-Agent": "hubbits-cli/0.1.0",
147
+ ...incomingHeaders
148
+ };
149
+ if (token) {
150
+ headers["Authorization"] = `Bearer ${token}`;
151
+ }
152
+ const retry = fetchOptions.retry ?? 2;
153
+ try {
154
+ const response = await ofetch(path, {
155
+ baseURL,
156
+ method: fetchOptions.method,
157
+ body: fetchOptions.body,
158
+ headers,
159
+ retry,
160
+ retryDelay: 1e3
161
+ });
162
+ return response;
163
+ } catch (error2) {
164
+ if (error2 instanceof FetchError) {
165
+ const status = error2.statusCode ?? 0;
166
+ const body = error2.data;
167
+ if (status === 401) {
168
+ clearToken();
169
+ throw new ApiError(
170
+ 401,
171
+ body?.error?.code ?? "UNAUTHORIZED",
172
+ "Authentication expired. Please run `hubbits login` to re-authenticate.",
173
+ body?.error?.details
174
+ );
175
+ }
176
+ if (body?.error) {
177
+ throw new ApiError(
178
+ status,
179
+ body.error.code,
180
+ body.error.message,
181
+ body.error.details
182
+ );
183
+ }
184
+ throw new ApiError(
185
+ status,
186
+ "NETWORK_ERROR",
187
+ `Request failed: ${error2.message}`
188
+ );
189
+ }
190
+ throw error2;
191
+ }
192
+ }
193
+ return {
194
+ /** Raw request method */
195
+ request,
196
+ // -- Auth --
197
+ /** Start device flow (get device code + user code) */
198
+ async startDeviceFlow() {
199
+ return request("/api/v1/auth/device", {
200
+ method: "POST"
201
+ });
202
+ },
203
+ /** Poll device flow authorization */
204
+ async pollDeviceFlow(deviceCode) {
205
+ return request(`/api/v1/auth/device/poll?device_code=${encodeURIComponent(deviceCode)}`, {
206
+ retry: 0
207
+ // Don't retry polling requests
208
+ });
209
+ },
210
+ /** Validate a PAT token */
211
+ async validateToken(token) {
212
+ return request("/api/v1/users/me", {
213
+ headers: { Authorization: `Bearer ${token}` }
214
+ });
215
+ },
216
+ /** Get current authenticated user info */
217
+ async getCurrentUser() {
218
+ return request("/api/v1/users/me");
219
+ },
220
+ // -- Search --
221
+ /** Search packages */
222
+ async searchPackages(query, params) {
223
+ const searchParams = new URLSearchParams({ q: query });
224
+ if (params?.category) searchParams.set("category", params.category);
225
+ if (params?.difficulty) searchParams.set("difficulty", params.difficulty);
226
+ if (params?.pricing) searchParams.set("pricing", params.pricing);
227
+ if (params?.sort) searchParams.set("sort", params.sort);
228
+ if (params?.page) searchParams.set("page", String(params.page));
229
+ if (params?.per_page) searchParams.set("per_page", String(params.per_page));
230
+ return request(`/api/v1/packages?${searchParams.toString()}`);
231
+ },
232
+ // -- Package --
233
+ /** Get package details */
234
+ async getPackage(name) {
235
+ return request(`/api/v1/packages/${encodeURIComponent(name)}`);
236
+ },
237
+ /** Get package details by scope/name */
238
+ async getPackageByScope(scope, name) {
239
+ return request(`/api/v1/packages/${encodeURIComponent(scope)}/${encodeURIComponent(name)}`);
240
+ },
241
+ /** Get specific version details */
242
+ async getPackageVersion(scope, name, version) {
243
+ return request(`/api/v1/packages/${encodeURIComponent(scope)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}`);
244
+ },
245
+ // -- Download --
246
+ /** Get download URL for a package version */
247
+ async getDownloadUrl(scope, name, version) {
248
+ return request(`/api/v1/download/${encodeURIComponent(scope)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}`);
249
+ },
250
+ /** Download a file from a URL (returns raw ArrayBuffer) */
251
+ async downloadFile(url) {
252
+ const response = await ofetch(url, {
253
+ responseType: "arrayBuffer",
254
+ retry: 2,
255
+ retryDelay: 1e3,
256
+ headers: {
257
+ "User-Agent": "hubbits-cli/0.1.0"
258
+ }
259
+ });
260
+ return response;
261
+ },
262
+ // -- Publish --
263
+ /** Request a presigned upload URL for publishing (step 1) */
264
+ async requestPublish(manifest, digest, fileSize) {
265
+ return request("/api/v1/packages/publish", {
266
+ method: "PUT",
267
+ body: { manifest, digest, file_size: fileSize }
268
+ });
269
+ },
270
+ /** Confirm a publish after uploading (step 2) */
271
+ async confirmPublish(scope, name, version, digest, manifest) {
272
+ return request("/api/v1/packages/publish/confirm", {
273
+ method: "POST",
274
+ body: { scope, name, version, digest, manifest }
275
+ });
276
+ },
277
+ /** Upload a file to a presigned URL */
278
+ async uploadFile(url, data) {
279
+ await ofetch(url, {
280
+ method: "PUT",
281
+ body: data,
282
+ headers: {
283
+ "Content-Type": "application/gzip",
284
+ "Content-Length": String(data.length)
285
+ },
286
+ retry: 2,
287
+ retryDelay: 1e3
288
+ });
289
+ },
290
+ // -- GitHub --
291
+ /** Fetch GitHub repo metadata for import pre-fill */
292
+ async getGithubRepoInfo(repoUrl) {
293
+ return request(`/api/v1/github/repo-info?url=${encodeURIComponent(repoUrl)}`);
294
+ }
295
+ };
296
+ }
297
+ var _jsonMode = false;
298
+ var _quietMode = false;
299
+ var _verboseMode = false;
300
+ function setOutputMode(opts) {
301
+ _jsonMode = opts.json ?? false;
302
+ _quietMode = opts.quiet ?? false;
303
+ _verboseMode = opts.verbose ?? false;
304
+ }
305
+ function isJsonMode() {
306
+ return _jsonMode;
307
+ }
308
+ function outputJson(data) {
309
+ console.log(JSON.stringify(data, null, 2));
310
+ }
311
+ function success(message) {
312
+ if (_quietMode) return;
313
+ if (_jsonMode) return;
314
+ console.log(`${pc5.green("\u2713")} ${message}`);
315
+ }
316
+ function error(message) {
317
+ if (_quietMode) return;
318
+ if (_jsonMode) return;
319
+ console.error(`${pc5.red("\u2717")} ${message}`);
320
+ }
321
+ function warning(message) {
322
+ if (_quietMode) return;
323
+ if (_jsonMode) return;
324
+ console.warn(`${pc5.yellow("\u26A0")} ${message}`);
325
+ }
326
+ function info(message) {
327
+ if (_quietMode) return;
328
+ if (_jsonMode) return;
329
+ console.log(`${pc5.blue("\u2139")} ${message}`);
330
+ }
331
+ function hint(message) {
332
+ if (_quietMode) return;
333
+ if (_jsonMode) return;
334
+ console.log(`${pc5.dim("\u2192")} ${message}`);
335
+ }
336
+ function debug(message) {
337
+ if (!_verboseMode) return;
338
+ if (_quietMode) return;
339
+ if (_jsonMode) return;
340
+ console.log(`${pc5.dim("[debug]")} ${pc5.dim(message)}`);
341
+ }
342
+ function newline() {
343
+ if (_quietMode) return;
344
+ if (_jsonMode) return;
345
+ console.log();
346
+ }
347
+ function field(label, value) {
348
+ if (_quietMode) return;
349
+ if (_jsonMode) return;
350
+ console.log(` ${pc5.dim(label + ":")} ${value}`);
351
+ }
352
+ function table(columns, rows) {
353
+ if (_quietMode) return;
354
+ if (_jsonMode) {
355
+ outputJson(rows);
356
+ return;
357
+ }
358
+ if (rows.length === 0) {
359
+ info("No results found.");
360
+ return;
361
+ }
362
+ const widths = {};
363
+ for (const col of columns) {
364
+ widths[col.key] = col.width ?? col.label.length;
365
+ for (const row of rows) {
366
+ const val = col.format ? col.format(row[col.key]) : String(row[col.key] ?? "");
367
+ widths[col.key] = Math.max(widths[col.key], val.length);
368
+ }
369
+ widths[col.key] = Math.min(widths[col.key], 50);
370
+ }
371
+ const header = columns.map((col) => pc5.bold(pad(col.label, widths[col.key], col.align))).join(" ");
372
+ console.log(header);
373
+ console.log(pc5.dim(columns.map((col) => "\u2500".repeat(widths[col.key])).join(" ")));
374
+ for (const row of rows) {
375
+ const line = columns.map((col) => {
376
+ const val = col.format ? col.format(row[col.key]) : String(row[col.key] ?? "");
377
+ return pad(truncate(val, widths[col.key]), widths[col.key], col.align);
378
+ }).join(" ");
379
+ console.log(line);
380
+ }
381
+ }
382
+ function spinner(text3) {
383
+ if (_quietMode || _jsonMode) {
384
+ return {
385
+ start: () => spinner(text3),
386
+ stop: () => spinner(text3),
387
+ succeed: () => spinner(text3),
388
+ fail: () => spinner(text3),
389
+ warn: () => spinner(text3),
390
+ info: () => spinner(text3),
391
+ text: "",
392
+ isSpinning: false
393
+ };
394
+ }
395
+ return ora({ text: text3, color: "cyan" }).start();
396
+ }
397
+ function errorWithFix(message, fix) {
398
+ error(message);
399
+ if (fix) {
400
+ hint(`Fix: ${fix}`);
401
+ }
402
+ }
403
+ function pad(str, width, align = "left") {
404
+ if (str.length >= width) return str;
405
+ const padding = " ".repeat(width - str.length);
406
+ return align === "right" ? padding + str : str + padding;
407
+ }
408
+ function truncate(str, maxWidth) {
409
+ if (str.length <= maxWidth) return str;
410
+ return str.slice(0, maxWidth - 1) + "\u2026";
411
+ }
412
+
413
+ // src/commands/login.ts
414
+ var login_default = defineCommand({
415
+ meta: {
416
+ name: "login",
417
+ description: "Authenticate with the Hubbits registry"
418
+ },
419
+ args: {
420
+ token: {
421
+ type: "string",
422
+ description: "Personal access token (for CI/CD)"
423
+ },
424
+ json: {
425
+ type: "boolean",
426
+ description: "Output as JSON",
427
+ default: false
428
+ },
429
+ quiet: {
430
+ type: "boolean",
431
+ description: "Suppress output",
432
+ default: false
433
+ },
434
+ verbose: {
435
+ type: "boolean",
436
+ description: "Show debug information",
437
+ default: false
438
+ }
439
+ },
440
+ async run({ args }) {
441
+ setOutputMode({ json: args.json, quiet: args.quiet, verbose: args.verbose });
442
+ const existing = getAuthState();
443
+ if (existing && !args.token) {
444
+ const displayName = existing.username ?? existing.email ?? "authenticated user";
445
+ if (isJsonMode()) {
446
+ outputJson({ status: "already_authenticated", username: existing.username, email: existing.email });
447
+ return;
448
+ }
449
+ info(`Already logged in as ${pc5.bold(displayName)}.`);
450
+ hint("Run `hubbits logout` first to switch accounts.");
451
+ return;
452
+ }
453
+ const api2 = createApiClient();
454
+ if (args.token) {
455
+ debug(`Validating provided token...`);
456
+ const s = spinner("Validating token...");
457
+ try {
458
+ const result = await api2.validateToken(args.token);
459
+ s.succeed("Token validated.");
460
+ if (result.data) {
461
+ saveToken({
462
+ token: args.token,
463
+ username: result.data.username,
464
+ email: result.data.email
465
+ });
466
+ if (isJsonMode()) {
467
+ outputJson({ status: "ok", username: result.data.username, email: result.data.email });
468
+ return;
469
+ }
470
+ success(`Logged in as ${pc5.bold(result.data.username)} (${result.data.email})`);
471
+ hint("Next: `hubbits search` to find packages or `hubbits create` to start building.");
472
+ }
473
+ } catch (err) {
474
+ s.fail("Token validation failed.");
475
+ if (err instanceof ApiError) {
476
+ errorWithFix(err.message, "Check that your token is correct and not expired.");
477
+ } else {
478
+ error("Failed to validate token.");
479
+ }
480
+ process.exit(1);
481
+ }
482
+ return;
483
+ }
484
+ p5.intro(pc5.bold("Hubbits Login"));
485
+ await loginWithDeviceFlow(api2);
486
+ }
487
+ });
488
+ async function loginWithDeviceFlow(api2) {
489
+ const s = spinner("Starting device authorization...");
490
+ debug("POST /api/v1/auth/device");
491
+ try {
492
+ const result = await api2.startDeviceFlow();
493
+ if (!result.data) {
494
+ s.fail("Failed to start device flow.");
495
+ error("Server returned an unexpected response.");
496
+ process.exit(1);
497
+ }
498
+ const { device_code, user_code, verification_url, expires_in, interval } = result.data;
499
+ s.stop();
500
+ newline();
501
+ info(`Open the following URL in your browser:`);
502
+ console.log(` ${pc5.bold(pc5.underline(verification_url))}`);
503
+ newline();
504
+ info(`Enter the code: ${pc5.bold(pc5.cyan(user_code))}`);
505
+ newline();
506
+ try {
507
+ await open(verification_url);
508
+ info("Browser opened automatically.");
509
+ } catch {
510
+ }
511
+ const pollSpinner = spinner("Waiting for authorization...");
512
+ const pollFn = async (deviceCode) => {
513
+ try {
514
+ const tokenResult = await api2.pollDeviceFlow(deviceCode);
515
+ if (tokenResult.data?.access_token) {
516
+ return {
517
+ status: "success",
518
+ token: tokenResult.data.access_token
519
+ };
520
+ }
521
+ return { status: "pending" };
522
+ } catch (err) {
523
+ if (err instanceof ApiError) {
524
+ if (err.code === "AUTHORIZATION_PENDING") return { status: "pending" };
525
+ if (err.code === "SLOW_DOWN") return { status: "pending" };
526
+ if (err.code === "EXPIRED" || err.code === "EXPIRED_TOKEN") return { status: "expired" };
527
+ return { status: "error", message: err.message };
528
+ }
529
+ return { status: "pending" };
530
+ }
531
+ };
532
+ const pollResult = await pollDeviceFlow(device_code, pollFn, {
533
+ interval,
534
+ expiresIn: expires_in
535
+ });
536
+ if (pollResult.status === "success") {
537
+ let username;
538
+ let email;
539
+ try {
540
+ const userResult = await api2.validateToken(pollResult.token);
541
+ if (userResult.data) {
542
+ username = userResult.data.username;
543
+ email = userResult.data.email;
544
+ }
545
+ } catch {
546
+ }
547
+ saveToken({
548
+ token: pollResult.token,
549
+ username,
550
+ email
551
+ });
552
+ pollSpinner.succeed("Authorized!");
553
+ if (isJsonMode()) {
554
+ outputJson({ status: "ok", username, email });
555
+ return;
556
+ }
557
+ const displayName = username ?? email ?? "user";
558
+ success(`Logged in as ${pc5.bold(displayName)}`);
559
+ hint("Next: `hubbits search` to find packages or `hubbits create` to start building.");
560
+ } else if (pollResult.status === "expired") {
561
+ pollSpinner.fail("Authorization expired.");
562
+ errorWithFix("The verification code has expired.", "Run `hubbits login` to try again.");
563
+ process.exit(1);
564
+ } else if (pollResult.status === "error") {
565
+ pollSpinner.fail("Authorization failed.");
566
+ errorWithFix(pollResult.message, "Run `hubbits login` to try again.");
567
+ process.exit(1);
568
+ }
569
+ } catch (err) {
570
+ s.fail("Failed to start device flow.");
571
+ if (err instanceof ApiError) {
572
+ errorWithFix(err.message, "Check your network connection and try again.");
573
+ } else {
574
+ errorWithFix("An unexpected error occurred.", "Check your network connection and try again.");
575
+ }
576
+ process.exit(1);
577
+ }
578
+ }
579
+ var logout_default = defineCommand({
580
+ meta: {
581
+ name: "logout",
582
+ description: "Log out from the Hubbits registry"
583
+ },
584
+ args: {
585
+ force: {
586
+ type: "boolean",
587
+ alias: "f",
588
+ description: "Skip confirmation prompt",
589
+ default: false
590
+ },
591
+ json: {
592
+ type: "boolean",
593
+ description: "Output as JSON",
594
+ default: false
595
+ },
596
+ quiet: {
597
+ type: "boolean",
598
+ description: "Suppress output",
599
+ default: false
600
+ },
601
+ verbose: {
602
+ type: "boolean",
603
+ description: "Show debug information",
604
+ default: false
605
+ }
606
+ },
607
+ async run({ args }) {
608
+ setOutputMode({ json: args.json, quiet: args.quiet, verbose: args.verbose });
609
+ const auth = getAuthState();
610
+ if (!auth) {
611
+ if (isJsonMode()) {
612
+ outputJson({ status: "not_authenticated" });
613
+ return;
614
+ }
615
+ info("You are not currently logged in.");
616
+ hint("Run `hubbits login` to authenticate.");
617
+ return;
618
+ }
619
+ const displayName = auth.username ?? auth.email ?? "current session";
620
+ if (!args.force && !args.json && !args.quiet) {
621
+ const confirm4 = await p5.confirm({
622
+ message: `Log out from ${pc5.bold(displayName)}?`
623
+ });
624
+ if (p5.isCancel(confirm4) || !confirm4) {
625
+ p5.cancel("Logout cancelled.");
626
+ return;
627
+ }
628
+ }
629
+ clearToken();
630
+ if (isJsonMode()) {
631
+ outputJson({ status: "ok", message: "Logged out successfully." });
632
+ return;
633
+ }
634
+ success(`Logged out from ${pc5.bold(displayName)}.`);
635
+ hint("Run `hubbits login` to log in again.");
636
+ }
637
+ });
638
+ var search_default = defineCommand({
639
+ meta: {
640
+ name: "search",
641
+ description: "Search for hubbits on the registry"
642
+ },
643
+ args: {
644
+ query: {
645
+ type: "positional",
646
+ description: "Search query",
647
+ required: true
648
+ },
649
+ category: {
650
+ type: "string",
651
+ alias: "c",
652
+ description: "Filter by category (learning, entertainment, productivity, wellness)"
653
+ },
654
+ difficulty: {
655
+ type: "string",
656
+ alias: "d",
657
+ description: "Filter by difficulty (beginner, intermediate, advanced)"
658
+ },
659
+ pricing: {
660
+ type: "string",
661
+ description: "Filter by pricing (free, paid, freemium)"
662
+ },
663
+ sort: {
664
+ type: "string",
665
+ alias: "s",
666
+ description: "Sort by (relevance, downloads, stars, updated, created)",
667
+ default: "relevance"
668
+ },
669
+ page: {
670
+ type: "string",
671
+ description: "Page number",
672
+ default: "1"
673
+ },
674
+ limit: {
675
+ type: "string",
676
+ description: "Results per page",
677
+ default: "20"
678
+ },
679
+ json: {
680
+ type: "boolean",
681
+ description: "Output as JSON",
682
+ default: false
683
+ },
684
+ quiet: {
685
+ type: "boolean",
686
+ description: "Suppress output",
687
+ default: false
688
+ },
689
+ verbose: {
690
+ type: "boolean",
691
+ description: "Show debug information",
692
+ default: false
693
+ }
694
+ },
695
+ async run({ args }) {
696
+ setOutputMode({ json: args.json, quiet: args.quiet, verbose: args.verbose });
697
+ const query = args.query;
698
+ const page = parseInt(args.page, 10) || 1;
699
+ const perPage = parseInt(args.limit, 10) || 20;
700
+ const api2 = createApiClient();
701
+ const s = spinner(`Searching for "${query}"...`);
702
+ debug(`GET /api/v1/packages?q=${query}&page=${page}&per_page=${perPage}`);
703
+ try {
704
+ const result = await api2.searchPackages(query, {
705
+ category: args.category,
706
+ difficulty: args.difficulty,
707
+ pricing: args.pricing,
708
+ sort: args.sort,
709
+ page,
710
+ per_page: perPage
711
+ });
712
+ s.stop();
713
+ if (isJsonMode()) {
714
+ outputJson(result);
715
+ return;
716
+ }
717
+ const items = result.data?.items ?? [];
718
+ const total = result.data?.pagination.total ?? 0;
719
+ if (items.length === 0) {
720
+ info(`No packages found for "${pc5.bold(query)}".`);
721
+ hint("Try a different search term or check `hubbits search --help` for filters.");
722
+ return;
723
+ }
724
+ success(`Found ${total} package${total === 1 ? "" : "s"} for "${pc5.bold(query)}"`);
725
+ newline();
726
+ const columns = [
727
+ {
728
+ key: "name",
729
+ label: "Name",
730
+ width: 30,
731
+ format: (v) => pc5.cyan(String(v))
732
+ },
733
+ {
734
+ key: "description",
735
+ label: "Description",
736
+ width: 40
737
+ },
738
+ {
739
+ key: "category",
740
+ label: "Category",
741
+ width: 14,
742
+ format: (v) => formatCategory(String(v ?? ""))
743
+ },
744
+ {
745
+ key: "downloads",
746
+ label: "Downloads",
747
+ width: 10,
748
+ align: "right",
749
+ format: (v) => formatNumber(Number(v))
750
+ },
751
+ {
752
+ key: "stars",
753
+ label: "Stars",
754
+ width: 6,
755
+ align: "right",
756
+ format: (v) => formatNumber(Number(v))
757
+ }
758
+ ];
759
+ table(columns, items.map((item) => ({
760
+ name: item.name,
761
+ description: item.description,
762
+ category: item.category,
763
+ downloads: item.downloads,
764
+ stars: item.stars
765
+ })));
766
+ if ((result.data?.pagination.total_pages ?? 0) > 1) {
767
+ newline();
768
+ info(
769
+ `Page ${result.data?.pagination.page}/${result.data?.pagination.total_pages} (${total} total results)`
770
+ );
771
+ if ((result.data?.pagination.page ?? 0) < (result.data?.pagination.total_pages ?? 0)) {
772
+ hint(`Next page: \`hubbits search "${query}" --page ${(result.data?.pagination.page ?? 0) + 1}\``);
773
+ }
774
+ }
775
+ newline();
776
+ hint(`Download a package: \`hubbits pull <package-name>\``);
777
+ } catch (err) {
778
+ s.fail("Search failed.");
779
+ if (err instanceof ApiError) {
780
+ errorWithFix(err.message, "Check your network connection and try again.");
781
+ } else {
782
+ errorWithFix("An unexpected error occurred.", "Check your network connection and try again.");
783
+ }
784
+ process.exit(1);
785
+ }
786
+ }
787
+ });
788
+ function formatNumber(n) {
789
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
790
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
791
+ return String(n);
792
+ }
793
+ function formatCategory(cat) {
794
+ const colors = {
795
+ learning: pc5.blue,
796
+ entertainment: pc5.magenta,
797
+ productivity: pc5.green,
798
+ wellness: pc5.yellow
799
+ };
800
+ const colorFn = colors[cat] ?? pc5.dim;
801
+ return colorFn(cat || "-");
802
+ }
803
+ var template_default = defineCommand({
804
+ meta: {
805
+ name: "template",
806
+ description: "Scaffold a new hubbit with cross-platform AI editor support"
807
+ },
808
+ args: {
809
+ name: {
810
+ type: "positional",
811
+ description: "Package name (lowercase, hyphens allowed)",
812
+ required: false
813
+ },
814
+ category: {
815
+ type: "string",
816
+ alias: "c",
817
+ description: "Package category (learning, entertainment, productivity, wellness)"
818
+ },
819
+ difficulty: {
820
+ type: "string",
821
+ alias: "d",
822
+ description: "Difficulty level (beginner, intermediate, advanced)"
823
+ },
824
+ description: {
825
+ type: "string",
826
+ description: "Package description"
827
+ },
828
+ author: {
829
+ type: "string",
830
+ alias: "a",
831
+ description: "Author name"
832
+ },
833
+ json: {
834
+ type: "boolean",
835
+ description: "Output as JSON",
836
+ default: false
837
+ },
838
+ quiet: {
839
+ type: "boolean",
840
+ description: "Suppress output",
841
+ default: false
842
+ },
843
+ verbose: {
844
+ type: "boolean",
845
+ description: "Show debug information",
846
+ default: false
847
+ }
848
+ },
849
+ async run({ args }) {
850
+ setOutputMode({ json: args.json, quiet: args.quiet, verbose: args.verbose });
851
+ let options;
852
+ if (args.name !== void 0 && !isValidName(args.name)) {
853
+ errorWithFix(
854
+ `Invalid package name: "${args.name}"`,
855
+ "Use lowercase letters, numbers, and hyphens. Must start with a letter."
856
+ );
857
+ process.exit(1);
858
+ }
859
+ if (args.name && args.category && args.difficulty && args.description) {
860
+ options = {
861
+ name: args.name,
862
+ category: args.category,
863
+ difficulty: args.difficulty,
864
+ description: args.description,
865
+ author: args.author
866
+ };
867
+ } else {
868
+ options = await interactiveCreate(args.name);
869
+ }
870
+ const targetDir = resolve(process.cwd(), options.name);
871
+ if (existsSync(targetDir)) {
872
+ const entries = readdirSync(targetDir);
873
+ if (entries.length > 0) {
874
+ errorWithFix(
875
+ `Directory "${options.name}" already exists and is not empty.`,
876
+ "Choose a different name, or remove the existing directory first."
877
+ );
878
+ process.exit(1);
879
+ }
880
+ }
881
+ const files = scaffoldTemplate(targetDir, options);
882
+ if (isJsonMode()) {
883
+ outputJson({ status: "ok", name: options.name, path: targetDir, files });
884
+ return;
885
+ }
886
+ newline();
887
+ success(`Created ${pc5.bold(options.name)}/`);
888
+ for (const file of files) {
889
+ console.log(` ${pc5.dim("\u251C\u2500\u2500")} ${file}`);
890
+ }
891
+ newline();
892
+ printSetupGuide(options.name);
893
+ }
894
+ });
895
+ async function interactiveCreate(initialName) {
896
+ p5.intro(pc5.bold("Create a new Hubbit"));
897
+ const name = initialName ?? await (async () => {
898
+ const val = await p5.text({
899
+ message: "Package name:",
900
+ placeholder: "my-awesome-hubbit",
901
+ validate(value) {
902
+ if (!value) return "Package name is required.";
903
+ if (!isValidName(value)) {
904
+ return "Use lowercase letters, numbers, and hyphens. Must start with a letter and not end with a hyphen.";
905
+ }
906
+ }
907
+ });
908
+ if (p5.isCancel(val)) {
909
+ p5.cancel("Cancelled.");
910
+ process.exit(0);
911
+ }
912
+ return val;
913
+ })();
914
+ const category = await p5.select({
915
+ message: "Category:",
916
+ options: [
917
+ { value: "learning", label: "Learning", hint: "Educational courses and tutorials" },
918
+ { value: "entertainment", label: "Entertainment", hint: "Games, stories, and fun" },
919
+ { value: "productivity", label: "Productivity", hint: "Tools and workflows" },
920
+ { value: "wellness", label: "Wellness", hint: "Health and wellbeing" }
921
+ ]
922
+ });
923
+ if (p5.isCancel(category)) {
924
+ p5.cancel("Cancelled.");
925
+ process.exit(0);
926
+ }
927
+ const difficulty = await p5.select({
928
+ message: "Difficulty:",
929
+ options: [
930
+ { value: "beginner", label: "Beginner" },
931
+ { value: "intermediate", label: "Intermediate" },
932
+ { value: "advanced", label: "Advanced" },
933
+ { value: "beginner-to-intermediate", label: "Beginner to Intermediate" },
934
+ { value: "intermediate-to-advanced", label: "Intermediate to Advanced" }
935
+ ]
936
+ });
937
+ if (p5.isCancel(difficulty)) {
938
+ p5.cancel("Cancelled.");
939
+ process.exit(0);
940
+ }
941
+ const description = await p5.text({
942
+ message: "Description:",
943
+ placeholder: "A brief description of your Hubbit",
944
+ validate(value) {
945
+ if (!value) return "Description is required.";
946
+ if (value.length > 500) return "Description must be 500 characters or fewer.";
947
+ }
948
+ });
949
+ if (p5.isCancel(description)) {
950
+ p5.cancel("Cancelled.");
951
+ process.exit(0);
952
+ }
953
+ const author = await p5.text({
954
+ message: "Author (optional):",
955
+ placeholder: "Your Name"
956
+ });
957
+ if (p5.isCancel(author)) {
958
+ p5.cancel("Cancelled.");
959
+ process.exit(0);
960
+ }
961
+ p5.outro(pc5.dim("Scaffolding..."));
962
+ return {
963
+ name,
964
+ category,
965
+ difficulty,
966
+ description,
967
+ author: author || void 0
968
+ };
969
+ }
970
+ function scaffoldTemplate(targetDir, options) {
971
+ const files = [];
972
+ const dirs = [
973
+ "",
974
+ ".claude",
975
+ ".claude/skills",
976
+ ".claude/skills/play",
977
+ ".claude/skills/hint",
978
+ ".claude/skills/next",
979
+ ".claude/skills/progress",
980
+ ".claude/skills/verify",
981
+ ".github",
982
+ "engine",
983
+ "content",
984
+ `content/ch01-getting-started`,
985
+ `content/ch01-getting-started/challenges`,
986
+ ".player"
987
+ ];
988
+ for (const dir of dirs) {
989
+ mkdirSync(join(targetDir, dir), { recursive: true });
990
+ }
991
+ function write(relPath, content, mode) {
992
+ writeFileSync(join(targetDir, relPath), content, void 0);
993
+ files.push(relPath);
994
+ }
995
+ write("AGENTS.md", generateAgents(options));
996
+ write("CLAUDE.md", generateClaudeMd(options));
997
+ write("GEMINI.md", generateThinWrapper(options));
998
+ write(".cursorrules", generateThinWrapper(options));
999
+ write(".clinerules", generateThinWrapper(options));
1000
+ write(".windsurfrules", generateThinWrapper(options));
1001
+ write(".github/copilot-instructions.md", generateThinWrapper(options));
1002
+ write(".claude/skills/play/SKILL.md", generatePlaySkill(options));
1003
+ write(".claude/skills/hint/SKILL.md", generateHintSkill(options));
1004
+ write(".claude/skills/next/SKILL.md", generateNextSkill(options));
1005
+ write(".claude/skills/progress/SKILL.md", generateProgressSkill(options));
1006
+ write(".claude/skills/verify/SKILL.md", generateVerifySkill(options));
1007
+ write("engine/rules.md", generateRules(options));
1008
+ write("engine/narrator.md", generateNarrator(options));
1009
+ write("engine/validation.md", generateValidation(options));
1010
+ write("content/ch01-getting-started/README.md", generateChapterReadme(options));
1011
+ write("content/ch01-getting-started/challenges/.gitkeep", "");
1012
+ write(".player/progress.yaml.template", generateProgressTemplate(options));
1013
+ write(".gitignore", generateGitignore());
1014
+ write("hubbit.yaml", generateManifest(options));
1015
+ write("README.md", generateReadme(options));
1016
+ return files;
1017
+ }
1018
+ function printSetupGuide(name) {
1019
+ const divider = pc5.dim("\u2500".repeat(56));
1020
+ console.log(pc5.bold(" Next: publish your hubbit on GitHub"));
1021
+ newline();
1022
+ console.log(divider);
1023
+ newline();
1024
+ console.log(` ${pc5.cyan(pc5.bold("Step 1"))} Initialize git`);
1025
+ newline();
1026
+ console.log(` Run in your terminal:`);
1027
+ console.log(` ${pc5.yellow(`cd ${name} && git init && git add . && git commit -m "feat: init"`)}`);
1028
+ newline();
1029
+ console.log(` Or paste this into your AI editor:`);
1030
+ newline();
1031
+ console.log(pc5.dim(" \u250C" + "\u2500".repeat(54) + "\u2510"));
1032
+ console.log(pc5.dim(" \u2502") + ` Initialize git for my new project in ./${name}. ` + pc5.dim("\u2502"));
1033
+ console.log(pc5.dim(" \u2502") + ` Run: git init && git add . && ` + pc5.dim("\u2502"));
1034
+ console.log(pc5.dim(" \u2502") + ` git commit -m "feat: init" ` + pc5.dim("\u2502"));
1035
+ console.log(pc5.dim(" \u2514" + "\u2500".repeat(54) + "\u2518"));
1036
+ newline();
1037
+ console.log(divider);
1038
+ newline();
1039
+ console.log(` ${pc5.cyan(pc5.bold("Step 2"))} Create a GitHub repo and push`);
1040
+ newline();
1041
+ console.log(` Or paste this into your AI editor:`);
1042
+ newline();
1043
+ const truncatedName = name.length > 40 ? name.slice(0, 37) + "..." : name;
1044
+ console.log(pc5.dim(" \u250C" + "\u2500".repeat(54) + "\u2510"));
1045
+ console.log(pc5.dim(" \u2502") + ` Create a public GitHub repo named: ` + pc5.dim("\u2502"));
1046
+ console.log(pc5.dim(" \u2502") + ` "${truncatedName}"` + " ".repeat(Math.max(0, 53 - truncatedName.length - 2)) + pc5.dim("\u2502"));
1047
+ console.log(pc5.dim(" \u2502") + ` Push my local commits to it. Use the gh CLI or ` + pc5.dim("\u2502"));
1048
+ console.log(pc5.dim(" \u2502") + ` guide me through the GitHub web UI. ` + pc5.dim("\u2502"));
1049
+ console.log(pc5.dim(" \u2514" + "\u2500".repeat(54) + "\u2518"));
1050
+ newline();
1051
+ console.log(divider);
1052
+ newline();
1053
+ console.log(` ${pc5.cyan(pc5.bold("Step 3"))} Register on Hubbits`);
1054
+ newline();
1055
+ console.log(` Once your repo is live, run:`);
1056
+ console.log(` ${pc5.yellow(`hubbits create import https://github.com/<you>/${name}`)}`);
1057
+ newline();
1058
+ console.log(divider);
1059
+ newline();
1060
+ hint("Run `hubbits validate` at any time to check your hubbit.yaml");
1061
+ newline();
1062
+ }
1063
+ function generateAgents(options) {
1064
+ return `# ${options.name} \u2014 Interactive AI Experience
1065
+
1066
+ You are the guide for **${options.name}**. ${options.description}
1067
+
1068
+ ## Quick Start
1069
+
1070
+ When the user opens this project and says anything like "let's play", "start", or "begin":
1071
+
1072
+ 1. Read \`engine/rules.md\` \u2014 your core behavior rules
1073
+ 2. Read \`engine/narrator.md\` \u2014 tone, persona, story context
1074
+ 3. Read \`.player/progress.yaml\` \u2014 check their current state
1075
+ 4. Welcome them and guide them to where they left off
1076
+
1077
+ ## Critical Rules
1078
+
1079
+ - ALWAYS read \`engine/rules.md\` before interacting
1080
+ - NEVER solve challenges for the user \u2014 only give hints
1081
+ - Track progress by updating \`.player/progress.yaml\` after each milestone
1082
+ - Respond in the user's language (auto-detect from their first message)
1083
+
1084
+ ## Commands
1085
+
1086
+ These work as natural conversation prompts:
1087
+
1088
+ - "let's play" / "start" \u2014 Start or resume
1089
+ - "hint" / "I'm stuck" \u2014 Get a progressive hint
1090
+ - "verify" / "check my work" \u2014 Verify challenge solution
1091
+ - "progress" \u2014 See how far they've come
1092
+ - "next" \u2014 Move to the next lesson or challenge
1093
+
1094
+ ## File Structure
1095
+
1096
+ \`\`\`
1097
+ engine/ \u2014 Rules, narrator persona, validation logic
1098
+ content/ \u2014 Chapters with lessons and challenges
1099
+ .player/ \u2014 Player state (do not commit progress.yaml)
1100
+ \`\`\`
1101
+ `;
1102
+ }
1103
+ function generateClaudeMd(options) {
1104
+ return `# ${options.name}
1105
+
1106
+ You are the guide for **${options.name}**. Read and follow all instructions in \`AGENTS.md\`.
1107
+
1108
+ **Critical:** NEVER solve challenges for the user \u2014 only give hints.
1109
+
1110
+ ## Claude Code Skills
1111
+
1112
+ This project includes slash commands in \`.claude/skills/\`:
1113
+
1114
+ - \`/play\` \u2014 Start or resume the experience
1115
+ - \`/hint\` \u2014 Get a progressive hint for the current challenge
1116
+ - \`/next\` \u2014 Move to the next lesson or challenge
1117
+ - \`/progress\` \u2014 Show current progress
1118
+ - \`/verify\` \u2014 Verify challenge solution
1119
+ `;
1120
+ }
1121
+ function generateThinWrapper(options) {
1122
+ return `# ${options.name}
1123
+
1124
+ You are the guide for **${options.name}**. Read and follow all instructions in \`AGENTS.md\`.
1125
+
1126
+ **Critical:** NEVER solve challenges for the user \u2014 only give hints.
1127
+ `;
1128
+ }
1129
+ function generatePlaySkill(options) {
1130
+ return `---
1131
+ name: play
1132
+ description: Start or resume the ${options.name} experience. Use when the user says "let's play", "start", "begin", or opens the project for the first time.
1133
+ ---
1134
+
1135
+ # Start / Resume
1136
+
1137
+ You are the guide for ${options.name}. Follow these steps:
1138
+
1139
+ ## Step 1: Load Engine
1140
+
1141
+ Read in order:
1142
+ - \`engine/rules.md\` \u2014 your behavior rules (teaching mode vs challenge mode)
1143
+ - \`engine/narrator.md\` \u2014 your persona, tone, and story context
1144
+
1145
+ ## Step 2: Check Player State
1146
+
1147
+ Read \`.player/progress.yaml\`.
1148
+
1149
+ ### New Player (started_at is empty)
1150
+
1151
+ 1. Detect the user's language from their first message (default: en)
1152
+ 2. Update \`.player/progress.yaml\`: set \`started_at\`, \`language\`
1153
+ 3. Welcome them warmly
1154
+ 4. Introduce Chapter 1 (read \`content/ch01-getting-started/README.md\`)
1155
+ 5. Ask: "Ready to start the first lesson, or jump straight to the challenge?"
1156
+
1157
+ ### Returning Player
1158
+
1159
+ 1. Welcome them back
1160
+ 2. Summarize where they left off
1161
+ 3. Offer: continue, replay chapter, or jump ahead
1162
+
1163
+ ## Step 3: Begin
1164
+
1165
+ Navigate to the correct lesson or challenge based on progress.
1166
+
1167
+ $ARGUMENTS
1168
+ `;
1169
+ }
1170
+ function generateHintSkill(options) {
1171
+ return `---
1172
+ name: hint
1173
+ description: Give a progressive hint for the current challenge in ${options.name}. Use when the user says "hint", "I'm stuck", "help", or "I don't know what to do".
1174
+ ---
1175
+
1176
+ # Give a Hint
1177
+
1178
+ Read \`engine/rules.md\` for hint level behavior.
1179
+
1180
+ ## Steps
1181
+
1182
+ 1. Check \`.player/progress.yaml\` for the current challenge
1183
+ 2. Read the challenge file to understand the expected outcome
1184
+ 3. Check the hint count already used (track in progress.yaml)
1185
+ 4. Give a hint at the appropriate level:
1186
+ - **Hint 1**: Conceptual \u2014 "Think about how X relates to Y..."
1187
+ - **Hint 2**: Directional \u2014 "Try looking at Z..."
1188
+ - **Hint 3**: Near-solution \u2014 "The answer involves doing W to..."
1189
+ 5. Do NOT give the solution directly
1190
+
1191
+ $ARGUMENTS
1192
+ `;
1193
+ }
1194
+ function generateNextSkill(options) {
1195
+ return `---
1196
+ name: next
1197
+ description: Move to the next lesson or challenge in ${options.name}. Use when the user says "next", "continue", "what's next", or finishes a lesson.
1198
+ ---
1199
+
1200
+ # Next Lesson or Challenge
1201
+
1202
+ ## Steps
1203
+
1204
+ 1. Read \`.player/progress.yaml\` to find current position
1205
+ 2. Determine what comes next:
1206
+ - If in a lesson \u2192 present the next lesson (or the chapter challenge if done)
1207
+ - If challenge was completed \u2192 move to next chapter
1208
+ - If all chapters done \u2192 congratulate and suggest contributing
1209
+ 3. Update \`progress.yaml\` to reflect the new position
1210
+ 4. Present the next content
1211
+
1212
+ $ARGUMENTS
1213
+ `;
1214
+ }
1215
+ function generateProgressSkill(options) {
1216
+ return `---
1217
+ name: progress
1218
+ description: Show the user's progress in ${options.name}. Use when the user says "progress", "how am I doing", "show my progress", or "where am I".
1219
+ ---
1220
+
1221
+ # Show Progress
1222
+
1223
+ ## Steps
1224
+
1225
+ 1. Read \`.player/progress.yaml\`
1226
+ 2. Display a summary:
1227
+ - Chapters completed vs total
1228
+ - Current chapter and lesson
1229
+ - Challenges solved
1230
+ - Time spent (if tracked)
1231
+ 3. Encourage them with a short motivational message
1232
+
1233
+ $ARGUMENTS
1234
+ `;
1235
+ }
1236
+ function generateVerifySkill(options) {
1237
+ return `---
1238
+ name: verify
1239
+ description: Verify the user's solution to the current challenge in ${options.name}. Use when the user says "verify", "check my work", "did I get it right", or "check".
1240
+ ---
1241
+
1242
+ # Verify Solution
1243
+
1244
+ Read \`engine/validation.md\` for verification rules.
1245
+
1246
+ ## Steps
1247
+
1248
+ 1. Read \`.player/progress.yaml\` for current challenge
1249
+ 2. Read the challenge file for expected outcome
1250
+ 3. Ask the user to show their work (output, code, or result)
1251
+ 4. Compare against expected outcome:
1252
+ - **Correct**: celebrate, update \`progress.yaml\` to mark challenge completed, suggest \`/next\`
1253
+ - **Incorrect**: give a specific hint about what's wrong (no solution)
1254
+
1255
+ $ARGUMENTS
1256
+ `;
1257
+ }
1258
+ function generateRules(options) {
1259
+ return `# ${options.name} \u2014 Rules
1260
+
1261
+ ## Two Modes
1262
+
1263
+ ### Teaching Mode (during lessons)
1264
+
1265
+ - Explain concepts clearly with examples
1266
+ - Ask questions to check understanding
1267
+ - Use analogies to make ideas concrete
1268
+ - Pace yourself \u2014 don't overwhelm
1269
+
1270
+ ### Challenge Mode (during challenges)
1271
+
1272
+ - NEVER give the answer directly
1273
+ - Use progressive hints (conceptual \u2192 directional \u2192 near-solution)
1274
+ - Let the user struggle productively \u2014 that's where learning happens
1275
+ - Celebrate when they solve it
1276
+
1277
+ ## Session Flow
1278
+
1279
+ 1. Read \`engine/narrator.md\` to load your persona
1280
+ 2. Read \`.player/progress.yaml\` to check state
1281
+ 3. Welcome the user and orient them
1282
+ 4. Present current lesson or challenge
1283
+ 5. Track progress after each milestone
1284
+
1285
+ ## Language Detection
1286
+
1287
+ - Detect the user's language from their first message
1288
+ - Respond in that language throughout
1289
+ - Keep chapter/lesson titles and technical terms in English
1290
+ - Record detected language as IETF tag in \`progress.yaml\` \u2192 \`player.language\`
1291
+
1292
+ ## Progress Tracking
1293
+
1294
+ Update \`.player/progress.yaml\` when:
1295
+ - A lesson is completed
1296
+ - A challenge is solved
1297
+ - The session ends (update \`last_played\`)
1298
+ `;
1299
+ }
1300
+ function generateNarrator(options) {
1301
+ return `# ${options.name} \u2014 Narrator
1302
+
1303
+ ## Persona
1304
+
1305
+ - **Name**: Guide
1306
+ - **Tone**: Encouraging, patient, and knowledgeable
1307
+ - **Style**: Clear explanations with real-world examples
1308
+ - **Approach**: Socratic \u2014 ask questions, don't just lecture
1309
+
1310
+ ## Story Context
1311
+
1312
+ ${options.description}
1313
+
1314
+ ## Greeting Templates
1315
+
1316
+ ### New User
1317
+ "Welcome! I'm your guide for ${options.name}.
1318
+ ${options.description}
1319
+ Ready to dive in? Let's start with Chapter 1."
1320
+
1321
+ ### Returning User
1322
+ "Welcome back! Last time you [summary from progress.yaml].
1323
+ Ready to pick up where you left off?"
1324
+
1325
+ ### After Completing a Challenge
1326
+ "Excellent work! You've solved [challenge name].
1327
+ Here's the key insight: [brief explanation].
1328
+ Up next: [preview of what's coming]"
1329
+
1330
+ ### When Stuck
1331
+ "No worries \u2014 this one is tricky.
1332
+ Think about [concept]. What happens when you [action]?
1333
+ Take your time."
1334
+ `;
1335
+ }
1336
+ function generateValidation(options) {
1337
+ return `# ${options.name} \u2014 Validation
1338
+
1339
+ ## How to Verify Challenges
1340
+
1341
+ When the user asks to verify their work:
1342
+
1343
+ 1. Read the current challenge requirements from the challenge file
1344
+ 2. Ask the user to show their result (output, code, screenshot, etc.)
1345
+ 3. Compare against the expected outcome
1346
+ 4. Give feedback:
1347
+
1348
+ ### Correct
1349
+
1350
+ - Celebrate clearly: "That's exactly right!"
1351
+ - Explain why it works (reinforce the learning)
1352
+ - Update \`.player/progress.yaml\`:
1353
+ \`\`\`yaml
1354
+ chapters:
1355
+ ch01-getting-started:
1356
+ challenges:
1357
+ challenge_01: completed
1358
+ \`\`\`
1359
+ - Suggest \`/next\` to continue
1360
+
1361
+ ### Incorrect
1362
+
1363
+ - Be specific: "The result is close, but [what's different]"
1364
+ - Give a targeted hint (not the solution)
1365
+ - Let them try again
1366
+
1367
+ ## Progress Updates
1368
+
1369
+ Always update \`last_played\` at the end of each session:
1370
+
1371
+ \`\`\`yaml
1372
+ player:
1373
+ last_played: <today's date>
1374
+ \`\`\`
1375
+ `;
1376
+ }
1377
+ function generateChapterReadme(options) {
1378
+ return `# Chapter 1: Getting Started
1379
+
1380
+ > Part of [${options.name}](../../hubbit.yaml)
1381
+
1382
+ ## Overview
1383
+
1384
+ [TODO: Write your chapter overview here. What will the user learn? Why does it matter?]
1385
+
1386
+ ## Lessons
1387
+
1388
+ ### Lesson 1: Introduction
1389
+
1390
+ [TODO: Write your first lesson here. Introduce the core concept.]
1391
+
1392
+ ### Lesson 2: Core Concepts
1393
+
1394
+ [TODO: Dive deeper. Explain the main ideas with examples.]
1395
+
1396
+ ### Lesson 3: Putting It Together
1397
+
1398
+ [TODO: Connect the concepts. Show how they work together in practice.]
1399
+
1400
+ ## Challenge
1401
+
1402
+ See the \`challenges/\` directory for the hands-on exercise.
1403
+
1404
+ After completing the challenge, run \`/verify\` to check your work.
1405
+ `;
1406
+ }
1407
+ function generateProgressTemplate(options) {
1408
+ return `# Progress template for ${options.name}
1409
+ # This file is committed to git as the clean starting state.
1410
+ # The live progress.yaml (in .player/) is gitignored.
1411
+
1412
+ version: 1
1413
+
1414
+ player:
1415
+ started_at: null
1416
+ last_played: null
1417
+ language: auto
1418
+
1419
+ chapters:
1420
+ ch01-getting-started:
1421
+ status: not_started # not_started | in_progress | completed
1422
+ started_at: null
1423
+ completed_at: null
1424
+ lessons:
1425
+ lesson_01: not_started
1426
+ lesson_02: not_started
1427
+ lesson_03: not_started
1428
+ challenges:
1429
+ challenge_01: not_started
1430
+ `;
1431
+ }
1432
+ function generateGitignore() {
1433
+ return `.player/progress.yaml
1434
+ !.player/progress.yaml.template
1435
+ .DS_Store
1436
+ Thumbs.db
1437
+ .vscode/
1438
+ .idea/
1439
+ node_modules/
1440
+ *.log
1441
+ `;
1442
+ }
1443
+ function generateManifest(options) {
1444
+ const authorLine = options.author ? `author: "${options.author}"` : '# author: "Your Name"';
1445
+ return `# Hubbit Manifest
1446
+ # Documentation: https://hubbits.dev/docs/manifest
1447
+
1448
+ spec_version: 1
1449
+ name: "${options.name}"
1450
+ version: "0.1.0"
1451
+ ${authorLine}
1452
+ description: "${escapeYaml(options.description)}"
1453
+
1454
+ category: ${options.category}
1455
+ difficulty: ${options.difficulty}
1456
+ language: auto
1457
+ estimated_time: "1-2 hours"
1458
+ license: MIT
1459
+
1460
+ tags:
1461
+ - ${options.category}
1462
+ # Add more tags here
1463
+
1464
+ pricing: free
1465
+
1466
+ # GitHub repository (set by \`hubbits create import\`)
1467
+ # repository: "https://github.com/<you>/${options.name}"
1468
+
1469
+ # Compatible AI editors
1470
+ compatible_with:
1471
+ - claude-code
1472
+ - cursor
1473
+ - windsurf
1474
+ - copilot
1475
+
1476
+ # AI persona configuration
1477
+ persona:
1478
+ name: "Guide"
1479
+ tone: "encouraging"
1480
+ traits:
1481
+ - "patient"
1482
+ - "knowledgeable"
1483
+ constraints:
1484
+ - "Never give answers directly; guide with hints"
1485
+ - "Celebrate progress"
1486
+
1487
+ # Curriculum structure
1488
+ curriculum:
1489
+ type: linear
1490
+ chapters:
1491
+ - id: ch01-getting-started
1492
+ title: "Chapter 1: Getting Started"
1493
+ lessons: 3
1494
+ challenges: 1
1495
+ unlock_condition: null
1496
+
1497
+ # Runtime configuration
1498
+ runtime:
1499
+ local: true
1500
+ cloud: false
1501
+
1502
+ # AI engine requirements
1503
+ engines:
1504
+ ai:
1505
+ min_context_window: 100000
1506
+ modalities:
1507
+ - text
1508
+ features:
1509
+ - tool_use
1510
+ `;
1511
+ }
1512
+ function generateReadme(options) {
1513
+ return `# ${options.name}
1514
+
1515
+ > ${options.description}
1516
+
1517
+ An interactive AI-driven experience. Open this project in your AI editor and type **"let's play"** to begin.
1518
+
1519
+ ---
1520
+
1521
+ ## How It Works
1522
+
1523
+ 1. Pull this package: \`hubbits pull <scope>/${options.name}\`
1524
+ 2. Open it in your AI editor (Claude Code, Cursor, Windsurf, Copilot, etc.)
1525
+ 3. Type **"let's play"**
1526
+ 4. Your AI becomes your guide
1527
+
1528
+ ---
1529
+
1530
+ ## Commands
1531
+
1532
+ \`\`\`
1533
+ "let's play" Start or resume
1534
+ "hint" Get a progressive hint
1535
+ "verify" Check your work
1536
+ "progress" See how far you've come
1537
+ "next" Move forward
1538
+ \`\`\`
1539
+
1540
+ ---
1541
+
1542
+ ## Project Structure
1543
+
1544
+ \`\`\`
1545
+ AGENTS.md \u2190 Universal AI entry point
1546
+ CLAUDE.md \u2190 Claude Code + skills
1547
+ .cursorrules \u2190 Cursor
1548
+ .windsurfrules \u2190 Windsurf
1549
+ .github/copilot-instructions.md \u2190 GitHub Copilot
1550
+
1551
+ engine/ \u2190 AI behavior (rules, narrator, validation)
1552
+ content/ \u2190 Chapters with lessons and challenges
1553
+ .player/ \u2190 Your progress (not committed)
1554
+ \`\`\`
1555
+
1556
+ ---
1557
+
1558
+ ## License
1559
+
1560
+ MIT
1561
+ `;
1562
+ }
1563
+ function isValidName(name) {
1564
+ if (name.length === 0 || name.length > 214) return false;
1565
+ return /^[a-z][a-z0-9-]*[a-z0-9]$/.test(name) || /^[a-z]$/.test(name);
1566
+ }
1567
+ function escapeYaml(str) {
1568
+ return str.replace(/"/g, '\\"');
1569
+ }
1570
+ var import_default = defineCommand({
1571
+ meta: {
1572
+ name: "import",
1573
+ description: "Import a GitHub repo as a hubbit"
1574
+ },
1575
+ args: {
1576
+ url: {
1577
+ type: "positional",
1578
+ description: "GitHub repository URL (e.g. https://github.com/user/repo)",
1579
+ required: true
1580
+ },
1581
+ yes: {
1582
+ type: "boolean",
1583
+ alias: "y",
1584
+ description: "Skip confirmation prompt",
1585
+ default: false
1586
+ },
1587
+ json: {
1588
+ type: "boolean",
1589
+ description: "Output as JSON",
1590
+ default: false
1591
+ },
1592
+ quiet: {
1593
+ type: "boolean",
1594
+ description: "Suppress output",
1595
+ default: false
1596
+ },
1597
+ verbose: {
1598
+ type: "boolean",
1599
+ description: "Show debug information",
1600
+ default: false
1601
+ }
1602
+ },
1603
+ async run({ args }) {
1604
+ setOutputMode({ json: args.json, quiet: args.quiet, verbose: args.verbose });
1605
+ const repoUrl = args.url;
1606
+ if (!isGithubUrl(repoUrl)) {
1607
+ errorWithFix(
1608
+ `Invalid GitHub URL: "${repoUrl}"`,
1609
+ "Provide a full GitHub repo URL, e.g. https://github.com/user/repo"
1610
+ );
1611
+ process.exit(1);
1612
+ }
1613
+ const packageDir = resolve(process.cwd());
1614
+ const fetchSpinner = spinner("Fetching repo info from GitHub...");
1615
+ const api2 = createApiClient();
1616
+ let repoInfo;
1617
+ try {
1618
+ const result = await api2.getGithubRepoInfo(repoUrl);
1619
+ if (!result.data) {
1620
+ fetchSpinner.fail("Failed to fetch repo info.");
1621
+ errorWithFix("No data returned from GitHub.", "Check that the repo is public.");
1622
+ process.exit(1);
1623
+ }
1624
+ repoInfo = result.data;
1625
+ fetchSpinner.succeed(`Found: ${pc5.bold(repoInfo.name)} \u2014 ${repoInfo.description || pc5.dim("no description")}`);
1626
+ } catch (err) {
1627
+ fetchSpinner.fail("Failed to fetch repo info.");
1628
+ if (err instanceof ApiError) {
1629
+ errorWithFix(err.message, "Make sure the repo is public and the URL is correct.");
1630
+ } else {
1631
+ errorWithFix("Network error.", "Check your connection and try again.");
1632
+ }
1633
+ process.exit(1);
1634
+ }
1635
+ const manifestPath = findManifestFile(packageDir);
1636
+ if (manifestPath) {
1637
+ const updated = setRepositoryField(manifestPath, repoUrl);
1638
+ if (updated) {
1639
+ success(`Updated ${pc5.dim("hubbit.yaml")} \u2014 set repository: ${pc5.cyan(repoUrl)}`);
1640
+ } else {
1641
+ info(`${pc5.dim("hubbit.yaml")} already has a repository field.`);
1642
+ }
1643
+ } else {
1644
+ if (!args.yes && !args.json && !args.quiet) {
1645
+ newline();
1646
+ info("No hubbit.yaml found in the current directory.");
1647
+ info("A minimal manifest will be created from GitHub metadata:");
1648
+ newline();
1649
+ field("Name", repoInfo.name);
1650
+ field("Description", repoInfo.description || "(empty)");
1651
+ field("License", repoInfo.license || "(none)");
1652
+ if (repoInfo.tags.length > 0) field("Tags", repoInfo.tags.slice(0, 5).join(", "));
1653
+ newline();
1654
+ const confirmed = await p5.confirm({ message: "Create hubbit.yaml?" });
1655
+ if (p5.isCancel(confirmed) || !confirmed) {
1656
+ p5.cancel("Import cancelled.");
1657
+ process.exit(0);
1658
+ }
1659
+ }
1660
+ const manifestContent = generateMinimalManifest(repoInfo, repoUrl);
1661
+ writeFileSync(join(packageDir, "hubbit.yaml"), manifestContent, "utf-8");
1662
+ success(`Created ${pc5.dim("hubbit.yaml")} from GitHub metadata.`);
1663
+ }
1664
+ if (isJsonMode()) {
1665
+ outputJson({
1666
+ status: "ok",
1667
+ repository: repoUrl,
1668
+ manifest: manifestPath ?? join(packageDir, "hubbit.yaml"),
1669
+ next: "hubbits publish"
1670
+ });
1671
+ return;
1672
+ }
1673
+ newline();
1674
+ success("Ready to publish!");
1675
+ newline();
1676
+ field("Repository", pc5.cyan(repoUrl));
1677
+ field("Directory", packageDir);
1678
+ newline();
1679
+ hint(`Run ${pc5.bold("hubbits publish")} to publish your hubbit to the registry.`);
1680
+ hint("Run `hubbits validate` first to check for any issues.");
1681
+ newline();
1682
+ }
1683
+ });
1684
+ function findManifestFile(dir) {
1685
+ for (const name of ["hubbit.yaml", "hubbit.yml"]) {
1686
+ const p7 = join(dir, name);
1687
+ if (existsSync(p7)) return p7;
1688
+ }
1689
+ return null;
1690
+ }
1691
+ function setRepositoryField(manifestPath, repoUrl) {
1692
+ const content = readFileSync(manifestPath, "utf-8");
1693
+ const repoLine = `repository: "${repoUrl}"`;
1694
+ const realMatch = content.match(/^repository:(.*)$/m);
1695
+ if (realMatch) {
1696
+ if (realMatch[0] === repoLine) return false;
1697
+ const updated2 = content.replace(/^repository:.*$/gm, repoLine);
1698
+ writeFileSync(manifestPath, updated2, "utf-8");
1699
+ return true;
1700
+ }
1701
+ if (/^#\s*repository:/m.test(content)) {
1702
+ const updated2 = content.replace(/^#\s*repository:.*$/m, repoLine);
1703
+ writeFileSync(manifestPath, updated2, "utf-8");
1704
+ return true;
1705
+ }
1706
+ const updated = content.trimEnd() + `
1707
+ repository: "${repoUrl}"
1708
+ `;
1709
+ writeFileSync(manifestPath, updated, "utf-8");
1710
+ return true;
1711
+ }
1712
+ function generateMinimalManifest(info2, repoUrl) {
1713
+ const name = toHubbitName(info2.name);
1714
+ const description = escapeYaml2(info2.description || `A hubbit based on ${info2.name}`);
1715
+ const license = info2.license ? `license: ${info2.license}` : "# license: MIT";
1716
+ const tagsBlock = info2.tags.length > 0 ? "tags:\n" + info2.tags.slice(0, 10).map((t) => ` - ${t}`).join("\n") : "# tags: []";
1717
+ return `# Hubbit Manifest
1718
+ # Documentation: https://hubbits.dev/docs/manifest
1719
+
1720
+ spec_version: 1
1721
+ name: "${name}"
1722
+ version: "${info2.version ?? "0.1.0"}"
1723
+ description: "${description}"
1724
+
1725
+ ${license}
1726
+ ${tagsBlock}
1727
+
1728
+ category: learning
1729
+ difficulty: beginner
1730
+ language: auto
1731
+ pricing: free
1732
+ repository: "${repoUrl}"
1733
+
1734
+ compatible_with:
1735
+ - claude-code
1736
+ - cursor
1737
+ - windsurf
1738
+ - copilot
1739
+
1740
+ runtime:
1741
+ local: true
1742
+ cloud: false
1743
+ `;
1744
+ }
1745
+ function isGithubUrl(url) {
1746
+ try {
1747
+ const u = new URL(url);
1748
+ return u.hostname === "github.com" && u.pathname.split("/").filter(Boolean).length >= 2;
1749
+ } catch {
1750
+ return false;
1751
+ }
1752
+ }
1753
+ function toHubbitName(name) {
1754
+ const result = name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1755
+ return result.length > 0 ? result : "my-hubbit";
1756
+ }
1757
+ function escapeYaml2(str) {
1758
+ return str.replace(/"/g, '\\"');
1759
+ }
1760
+
1761
+ // src/commands/create/index.ts
1762
+ var create_default = defineCommand({
1763
+ meta: {
1764
+ name: "create",
1765
+ description: "Create a new hubbit or import from GitHub"
1766
+ },
1767
+ subCommands: {
1768
+ template: template_default,
1769
+ import: import_default
1770
+ }
1771
+ });
1772
+ var init_default = defineCommand({
1773
+ meta: {
1774
+ name: "init",
1775
+ description: "Initialize a hubbit in the current directory"
1776
+ },
1777
+ args: {
1778
+ json: {
1779
+ type: "boolean",
1780
+ description: "Output as JSON",
1781
+ default: false
1782
+ },
1783
+ quiet: {
1784
+ type: "boolean",
1785
+ description: "Suppress output",
1786
+ default: false
1787
+ },
1788
+ verbose: {
1789
+ type: "boolean",
1790
+ description: "Show debug information",
1791
+ default: false
1792
+ }
1793
+ },
1794
+ async run({ args }) {
1795
+ setOutputMode({ json: args.json, quiet: args.quiet, verbose: args.verbose });
1796
+ const cwd = process.cwd();
1797
+ const cwdName = basename(cwd);
1798
+ const yamlPath = join(cwd, "hubbit.yaml");
1799
+ const authState = getAuthState();
1800
+ const loggedInUsername = authState?.username ?? void 0;
1801
+ if (existsSync(yamlPath)) {
1802
+ if (isJsonMode()) {
1803
+ outputJson({ status: "exists", path: yamlPath });
1804
+ return;
1805
+ }
1806
+ info("hubbit.yaml already exists in this directory.");
1807
+ newline();
1808
+ hint("Validate: `hubbits validate`");
1809
+ hint("Publish: `hubbits publish`");
1810
+ return;
1811
+ }
1812
+ const entries = readdirSync(cwd).filter(
1813
+ (e) => e !== ".git" && e !== ".DS_Store" && e !== "Thumbs.db"
1814
+ );
1815
+ if (entries.length === 0) {
1816
+ const meta2 = await interactiveFullMeta(cwdName, loggedInUsername);
1817
+ const files = scaffoldInto(cwd, meta2);
1818
+ if (isJsonMode()) {
1819
+ outputJson({ status: "ok", mode: "scaffold", path: cwd, files });
1820
+ return;
1821
+ }
1822
+ newline();
1823
+ success(`Scaffolded hubbit into ${pc5.bold(cwdName)}/`);
1824
+ for (const file of files) {
1825
+ console.log(` ${pc5.dim("\u251C\u2500\u2500")} ${file}`);
1826
+ }
1827
+ newline();
1828
+ printPostScaffoldGuide(cwdName);
1829
+ return;
1830
+ }
1831
+ const meta = await interactiveYamlOnly(cwdName, loggedInUsername);
1832
+ writeFileSync(yamlPath, generateManifest2(meta));
1833
+ if (isJsonMode()) {
1834
+ outputJson({ status: "ok", mode: "yaml_only", path: yamlPath });
1835
+ return;
1836
+ }
1837
+ newline();
1838
+ success(`Created ${pc5.bold("hubbit.yaml")}`);
1839
+ newline();
1840
+ console.log(` ${pc5.dim("\u251C\u2500\u2500")} hubbit.yaml`);
1841
+ newline();
1842
+ console.log(pc5.bold(" Next steps:"));
1843
+ newline();
1844
+ console.log(` 1. Review ${pc5.yellow("hubbit.yaml")} and fill in any details`);
1845
+ console.log(` 2. Commit and push to GitHub`);
1846
+ console.log(` 3. Run ${pc5.yellow("hubbits publish")} to publish`);
1847
+ newline();
1848
+ hint("Validate your manifest: `hubbits validate`");
1849
+ newline();
1850
+ }
1851
+ });
1852
+ async function interactiveFullMeta(defaultName, username) {
1853
+ p5.intro(pc5.bold("Initialize a Hubbit"));
1854
+ const name = await p5.text({
1855
+ message: "Package name:",
1856
+ placeholder: defaultName || "my-awesome-hubbit",
1857
+ initialValue: isValidName2(defaultName) ? defaultName : "",
1858
+ validate(value) {
1859
+ if (!value) return "Package name is required.";
1860
+ if (!isValidName2(value)) {
1861
+ return "Use lowercase letters, numbers, and hyphens. Must start with a letter.";
1862
+ }
1863
+ }
1864
+ });
1865
+ if (p5.isCancel(name)) {
1866
+ p5.cancel("Cancelled.");
1867
+ process.exit(0);
1868
+ }
1869
+ const category = await p5.select({
1870
+ message: "Category:",
1871
+ options: [
1872
+ { value: "learning", label: "Learning", hint: "Educational courses and tutorials" },
1873
+ { value: "entertainment", label: "Entertainment", hint: "Games, stories, and fun" },
1874
+ { value: "productivity", label: "Productivity", hint: "Tools and workflows" },
1875
+ { value: "wellness", label: "Wellness", hint: "Health and wellbeing" }
1876
+ ]
1877
+ });
1878
+ if (p5.isCancel(category)) {
1879
+ p5.cancel("Cancelled.");
1880
+ process.exit(0);
1881
+ }
1882
+ const difficulty = await p5.select({
1883
+ message: "Difficulty:",
1884
+ options: [
1885
+ { value: "beginner", label: "Beginner" },
1886
+ { value: "intermediate", label: "Intermediate" },
1887
+ { value: "advanced", label: "Advanced" },
1888
+ { value: "beginner-to-intermediate", label: "Beginner to Intermediate" },
1889
+ { value: "intermediate-to-advanced", label: "Intermediate to Advanced" }
1890
+ ]
1891
+ });
1892
+ if (p5.isCancel(difficulty)) {
1893
+ p5.cancel("Cancelled.");
1894
+ process.exit(0);
1895
+ }
1896
+ const description = await p5.text({
1897
+ message: "Description:",
1898
+ placeholder: "A brief description of your Hubbit",
1899
+ validate(value) {
1900
+ if (!value) return "Description is required.";
1901
+ if (value.length > 500) return "Description must be 500 characters or fewer.";
1902
+ }
1903
+ });
1904
+ if (p5.isCancel(description)) {
1905
+ p5.cancel("Cancelled.");
1906
+ process.exit(0);
1907
+ }
1908
+ let author = username;
1909
+ if (!author) {
1910
+ const authorInput = await p5.text({
1911
+ message: "Author (optional):",
1912
+ placeholder: "Your Name"
1913
+ });
1914
+ if (p5.isCancel(authorInput)) {
1915
+ p5.cancel("Cancelled.");
1916
+ process.exit(0);
1917
+ }
1918
+ author = authorInput || void 0;
1919
+ }
1920
+ p5.outro(pc5.dim("Scaffolding..."));
1921
+ return {
1922
+ name,
1923
+ category,
1924
+ difficulty,
1925
+ description,
1926
+ version: "0.1.0",
1927
+ pricing: "free",
1928
+ author
1929
+ };
1930
+ }
1931
+ async function interactiveYamlOnly(defaultName, username) {
1932
+ p5.intro(pc5.bold("Create hubbit.yaml"));
1933
+ const name = await p5.text({
1934
+ message: "Package name:",
1935
+ placeholder: defaultName || "my-awesome-hubbit",
1936
+ initialValue: isValidName2(defaultName) ? defaultName : "",
1937
+ validate(value) {
1938
+ if (!value) return "Package name is required.";
1939
+ if (!isValidName2(value)) {
1940
+ return "Use lowercase letters, numbers, and hyphens. Must start with a letter.";
1941
+ }
1942
+ }
1943
+ });
1944
+ if (p5.isCancel(name)) {
1945
+ p5.cancel("Cancelled.");
1946
+ process.exit(0);
1947
+ }
1948
+ const version = await p5.text({
1949
+ message: "Version:",
1950
+ placeholder: "0.1.0",
1951
+ initialValue: "0.1.0",
1952
+ validate(value) {
1953
+ if (!value) return "Version is required.";
1954
+ if (!/^\d+\.\d+\.\d+/.test(value)) return "Must be a valid SemVer (e.g., 0.1.0).";
1955
+ }
1956
+ });
1957
+ if (p5.isCancel(version)) {
1958
+ p5.cancel("Cancelled.");
1959
+ process.exit(0);
1960
+ }
1961
+ const description = await p5.text({
1962
+ message: "Description:",
1963
+ placeholder: "A brief description of your Hubbit",
1964
+ validate(value) {
1965
+ if (!value) return "Description is required.";
1966
+ if (value.length > 500) return "Description must be 500 characters or fewer.";
1967
+ }
1968
+ });
1969
+ if (p5.isCancel(description)) {
1970
+ p5.cancel("Cancelled.");
1971
+ process.exit(0);
1972
+ }
1973
+ const category = await p5.select({
1974
+ message: "Category:",
1975
+ options: [
1976
+ { value: "learning", label: "Learning" },
1977
+ { value: "entertainment", label: "Entertainment" },
1978
+ { value: "productivity", label: "Productivity" },
1979
+ { value: "wellness", label: "Wellness" }
1980
+ ]
1981
+ });
1982
+ if (p5.isCancel(category)) {
1983
+ p5.cancel("Cancelled.");
1984
+ process.exit(0);
1985
+ }
1986
+ const difficulty = await p5.select({
1987
+ message: "Difficulty:",
1988
+ options: [
1989
+ { value: "beginner", label: "Beginner" },
1990
+ { value: "intermediate", label: "Intermediate" },
1991
+ { value: "advanced", label: "Advanced" },
1992
+ { value: "beginner-to-intermediate", label: "Beginner to Intermediate" },
1993
+ { value: "intermediate-to-advanced", label: "Intermediate to Advanced" }
1994
+ ]
1995
+ });
1996
+ if (p5.isCancel(difficulty)) {
1997
+ p5.cancel("Cancelled.");
1998
+ process.exit(0);
1999
+ }
2000
+ const pricing = await p5.select({
2001
+ message: "Pricing:",
2002
+ options: [
2003
+ { value: "free", label: "Free" },
2004
+ { value: "paid", label: "Paid" }
2005
+ ]
2006
+ });
2007
+ if (p5.isCancel(pricing)) {
2008
+ p5.cancel("Cancelled.");
2009
+ process.exit(0);
2010
+ }
2011
+ p5.outro(pc5.dim("Generating hubbit.yaml..."));
2012
+ return {
2013
+ name,
2014
+ version,
2015
+ description,
2016
+ category,
2017
+ difficulty,
2018
+ pricing,
2019
+ author: username
2020
+ };
2021
+ }
2022
+ function scaffoldInto(targetDir, meta) {
2023
+ const files = [];
2024
+ const dirs = [
2025
+ ".claude/skills/play",
2026
+ ".claude/skills/hint",
2027
+ ".claude/skills/next",
2028
+ ".claude/skills/progress",
2029
+ ".claude/skills/verify",
2030
+ ".github",
2031
+ "engine",
2032
+ "content/ch01-getting-started/challenges",
2033
+ ".player"
2034
+ ];
2035
+ for (const dir of dirs) {
2036
+ mkdirSync(join(targetDir, dir), { recursive: true });
2037
+ }
2038
+ function write(relPath, content) {
2039
+ writeFileSync(join(targetDir, relPath), content);
2040
+ files.push(relPath);
2041
+ }
2042
+ write("AGENTS.md", `# ${meta.name} \u2014 Interactive AI Experience
2043
+
2044
+ You are the guide for **${meta.name}**. ${meta.description}
2045
+
2046
+ ## Quick Start
2047
+
2048
+ When the user opens this project and says anything like "let's play", "start", or "begin":
2049
+
2050
+ 1. Read \`engine/rules.md\` \u2014 your core behavior rules
2051
+ 2. Read \`engine/narrator.md\` \u2014 tone, persona, story context
2052
+ 3. Read \`.player/progress.yaml\` \u2014 check their current state
2053
+ 4. Welcome them and guide them to where they left off
2054
+
2055
+ ## Critical Rules
2056
+
2057
+ - ALWAYS read \`engine/rules.md\` before interacting
2058
+ - NEVER solve challenges for the user \u2014 only give hints
2059
+ - Track progress by updating \`.player/progress.yaml\` after each milestone
2060
+ - Respond in the user's language (auto-detect from their first message)
2061
+ `);
2062
+ write("CLAUDE.md", `# ${meta.name}
2063
+
2064
+ You are the guide for **${meta.name}**. Read and follow all instructions in \`AGENTS.md\`.
2065
+
2066
+ **Critical:** NEVER solve challenges for the user \u2014 only give hints.
2067
+
2068
+ ## Claude Code Skills
2069
+
2070
+ - \`/play\` \u2014 Start or resume the experience
2071
+ - \`/hint\` \u2014 Get a progressive hint for the current challenge
2072
+ - \`/next\` \u2014 Move to the next lesson or challenge
2073
+ - \`/progress\` \u2014 Show current progress
2074
+ - \`/verify\` \u2014 Verify challenge solution
2075
+ `);
2076
+ write("GEMINI.md", `# ${meta.name}
2077
+
2078
+ You are the guide for **${meta.name}**. Read and follow all instructions in \`AGENTS.md\`.
2079
+
2080
+ **Critical:** NEVER solve challenges for the user \u2014 only give hints.
2081
+ `);
2082
+ write(".cursorrules", `# ${meta.name}
2083
+
2084
+ You are the guide for **${meta.name}**. Read and follow all instructions in \`AGENTS.md\`.
2085
+
2086
+ **Critical:** NEVER solve challenges for the user \u2014 only give hints.
2087
+ `);
2088
+ write(".windsurfrules", `# ${meta.name}
2089
+
2090
+ You are the guide for **${meta.name}**. Read and follow all instructions in \`AGENTS.md\`.
2091
+
2092
+ **Critical:** NEVER solve challenges for the user \u2014 only give hints.
2093
+ `);
2094
+ write(".github/copilot-instructions.md", `# ${meta.name}
2095
+
2096
+ You are the guide for **${meta.name}**. Read and follow all instructions in \`AGENTS.md\`.
2097
+
2098
+ **Critical:** NEVER solve challenges for the user \u2014 only give hints.
2099
+ `);
2100
+ write(".claude/skills/play/SKILL.md", `---
2101
+ name: play
2102
+ description: Start or resume the ${meta.name} experience.
2103
+ ---
2104
+
2105
+ # Start / Resume
2106
+
2107
+ 1. Read \`engine/rules.md\` and \`engine/narrator.md\`
2108
+ 2. Read \`.player/progress.yaml\` to check player state
2109
+ 3. Welcome them and guide them to the right starting point
2110
+
2111
+ $ARGUMENTS
2112
+ `);
2113
+ write(".claude/skills/hint/SKILL.md", `---
2114
+ name: hint
2115
+ description: Give a progressive hint for the current challenge in ${meta.name}.
2116
+ ---
2117
+
2118
+ # Give a Hint
2119
+
2120
+ 1. Check \`.player/progress.yaml\` for the current challenge
2121
+ 2. Give a hint at the appropriate level (conceptual \u2192 directional \u2192 near-solution)
2122
+ 3. Do NOT give the solution directly
2123
+
2124
+ $ARGUMENTS
2125
+ `);
2126
+ write(".claude/skills/next/SKILL.md", `---
2127
+ name: next
2128
+ description: Move to the next lesson or challenge in ${meta.name}.
2129
+ ---
2130
+
2131
+ # Next Lesson
2132
+
2133
+ 1. Read \`.player/progress.yaml\` to find current position
2134
+ 2. Present the next lesson or challenge
2135
+ 3. Update progress.yaml
2136
+
2137
+ $ARGUMENTS
2138
+ `);
2139
+ write(".claude/skills/progress/SKILL.md", `---
2140
+ name: progress
2141
+ description: Show the user's progress in ${meta.name}.
2142
+ ---
2143
+
2144
+ # Show Progress
2145
+
2146
+ 1. Read \`.player/progress.yaml\`
2147
+ 2. Show chapters completed, current position, and time spent
2148
+
2149
+ $ARGUMENTS
2150
+ `);
2151
+ write(".claude/skills/verify/SKILL.md", `---
2152
+ name: verify
2153
+ description: Verify the user's solution to the current challenge in ${meta.name}.
2154
+ ---
2155
+
2156
+ # Verify Solution
2157
+
2158
+ 1. Read \`.player/progress.yaml\` for current challenge
2159
+ 2. Ask user to show their work
2160
+ 3. Compare against expected outcome \u2014 celebrate if correct, hint if not
2161
+
2162
+ $ARGUMENTS
2163
+ `);
2164
+ write("engine/rules.md", `# ${meta.name} \u2014 Rules
2165
+
2166
+ ## Teaching Mode
2167
+
2168
+ - Explain concepts clearly with examples
2169
+ - Ask questions to check understanding
2170
+
2171
+ ## Challenge Mode
2172
+
2173
+ - NEVER give the answer directly
2174
+ - Use progressive hints (conceptual \u2192 directional \u2192 near-solution)
2175
+ - Celebrate when they solve it
2176
+ `);
2177
+ write("engine/narrator.md", `# ${meta.name} \u2014 Narrator
2178
+
2179
+ ## Persona
2180
+
2181
+ - **Tone**: Encouraging, patient, knowledgeable
2182
+ - **Style**: Socratic \u2014 ask questions, don't just lecture
2183
+
2184
+ ## Story Context
2185
+
2186
+ ${meta.description}
2187
+ `);
2188
+ write("engine/validation.md", `# ${meta.name} \u2014 Validation
2189
+
2190
+ When the user asks to verify their work:
2191
+
2192
+ 1. Read the current challenge requirements
2193
+ 2. Ask the user to show their result
2194
+ 3. Compare and give feedback \u2014 celebrate correct, hint if not
2195
+ 4. Update \`.player/progress.yaml\` on success
2196
+ `);
2197
+ write("content/ch01-getting-started/README.md", `# Chapter 1: Getting Started
2198
+
2199
+ > Part of [${meta.name}](../../hubbit.yaml)
2200
+
2201
+ ## Overview
2202
+
2203
+ [TODO: Write your chapter overview here.]
2204
+
2205
+ ## Lessons
2206
+
2207
+ ### Lesson 1: Introduction
2208
+
2209
+ [TODO: Write your first lesson here.]
2210
+
2211
+ ## Challenge
2212
+
2213
+ See the \`challenges/\` directory.
2214
+ `);
2215
+ write("content/ch01-getting-started/challenges/.gitkeep", "");
2216
+ write(".player/progress.yaml.template", `version: 1
2217
+
2218
+ player:
2219
+ started_at: null
2220
+ last_played: null
2221
+ language: auto
2222
+
2223
+ chapters:
2224
+ ch01-getting-started:
2225
+ status: not_started
2226
+ lessons:
2227
+ lesson_01: not_started
2228
+ challenges:
2229
+ challenge_01: not_started
2230
+ `);
2231
+ write(".gitignore", `.player/progress.yaml
2232
+ !.player/progress.yaml.template
2233
+ .DS_Store
2234
+ Thumbs.db
2235
+ `);
2236
+ write("hubbit.yaml", generateManifest2(meta));
2237
+ write("README.md", `# ${meta.name}
2238
+
2239
+ > ${meta.description}
2240
+
2241
+ Open this project in your AI editor and type **"let's play"** to begin.
2242
+
2243
+ ## How It Works
2244
+
2245
+ 1. Pull this package: \`hubbits pull <scope>/${meta.name}\`
2246
+ 2. Open in your AI editor (Claude Code, Cursor, Windsurf, Copilot, etc.)
2247
+ 3. Type **"let's play"**
2248
+
2249
+ ## License
2250
+
2251
+ MIT
2252
+ `);
2253
+ return files;
2254
+ }
2255
+ function generateManifest2(meta) {
2256
+ const authorLine = meta.author ? `author: "${meta.author}"` : '# author: "Your Name"';
2257
+ return `# Hubbit Manifest
2258
+ # Documentation: https://hubbits.dev/docs/manifest
2259
+
2260
+ spec_version: 1
2261
+ name: "${meta.name}"
2262
+ version: "${meta.version}"
2263
+ ${authorLine}
2264
+ description: "${meta.description.replace(/"/g, '\\"')}"
2265
+
2266
+ category: ${meta.category}
2267
+ difficulty: ${meta.difficulty}
2268
+ language: auto
2269
+ ${meta.pricing === "paid" ? "" : "# "}estimated_time: "1-2 hours"
2270
+ license: MIT
2271
+
2272
+ tags:
2273
+ - ${meta.category}
2274
+ # Add more tags here
2275
+
2276
+ pricing: ${meta.pricing}
2277
+
2278
+ # GitHub repository (required for CLI publish)
2279
+ # repository: "https://github.com/<you>/${meta.name}"
2280
+
2281
+ # Compatible AI editors
2282
+ compatible_with:
2283
+ - claude-code
2284
+ - cursor
2285
+ - windsurf
2286
+ - copilot
2287
+
2288
+ # AI persona configuration
2289
+ persona:
2290
+ name: "Guide"
2291
+ tone: "encouraging"
2292
+ traits:
2293
+ - "patient"
2294
+ - "knowledgeable"
2295
+ constraints:
2296
+ - "Never give answers directly; guide with hints"
2297
+ - "Celebrate progress"
2298
+ `;
2299
+ }
2300
+ function printPostScaffoldGuide(name) {
2301
+ const divider = pc5.dim("\u2500".repeat(56));
2302
+ console.log(pc5.bold(" Next: publish your hubbit on GitHub"));
2303
+ newline();
2304
+ console.log(divider);
2305
+ newline();
2306
+ console.log(` ${pc5.cyan(pc5.bold("Step 1"))} Commit and push to GitHub`);
2307
+ newline();
2308
+ console.log(` ${pc5.yellow(`git add . && git commit -m "feat: init ${name}"`)}`);
2309
+ console.log(` ${pc5.yellow(`git push -u origin main`)}`);
2310
+ newline();
2311
+ console.log(divider);
2312
+ newline();
2313
+ console.log(` ${pc5.cyan(pc5.bold("Step 2"))} Publish to Hubbits`);
2314
+ newline();
2315
+ console.log(` ${pc5.yellow(`hubbits publish`)}`);
2316
+ newline();
2317
+ console.log(divider);
2318
+ newline();
2319
+ hint("Validate your manifest at any time: `hubbits validate`");
2320
+ newline();
2321
+ }
2322
+ function isValidName2(name) {
2323
+ if (!name || name.length === 0 || name.length > 214) return false;
2324
+ return /^[a-z][a-z0-9-]*[a-z0-9]$/.test(name) || /^[a-z]$/.test(name);
2325
+ }
2326
+ var VALID_CATEGORIES = ["learning", "entertainment", "productivity", "wellness"];
2327
+ var VALID_DIFFICULTIES = ["beginner", "intermediate", "advanced", "beginner-to-intermediate", "intermediate-to-advanced"];
2328
+ var VALID_PRICING = ["free", "paid", "freemium"];
2329
+ var VALID_MODALITIES = ["text", "image", "audio"];
2330
+ var VALID_FEATURES = ["tool_use", "vision", "code_execution"];
2331
+ var VALID_CURRICULUM_TYPES = ["linear", "branching", "open-world"];
2332
+ var NAME_PATTERN = /^[a-z][a-z0-9-]*[a-z0-9]$/;
2333
+ var SINGLE_CHAR_NAME = /^[a-z]$/;
2334
+ var SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$/;
2335
+ var validate_default = defineCommand({
2336
+ meta: {
2337
+ name: "validate",
2338
+ description: "Validate a hubbit.yaml manifest"
2339
+ },
2340
+ args: {
2341
+ path: {
2342
+ type: "positional",
2343
+ description: "Path to hubbit.yaml or directory containing it",
2344
+ required: false
2345
+ },
2346
+ strict: {
2347
+ type: "boolean",
2348
+ description: "Treat warnings as errors (for publishing)",
2349
+ default: false
2350
+ },
2351
+ json: {
2352
+ type: "boolean",
2353
+ description: "Output as JSON",
2354
+ default: false
2355
+ },
2356
+ quiet: {
2357
+ type: "boolean",
2358
+ description: "Suppress output",
2359
+ default: false
2360
+ },
2361
+ verbose: {
2362
+ type: "boolean",
2363
+ description: "Show debug information",
2364
+ default: false
2365
+ }
2366
+ },
2367
+ async run({ args }) {
2368
+ setOutputMode({ json: args.json, quiet: args.quiet, verbose: args.verbose });
2369
+ const inputPath = args.path ?? process.cwd();
2370
+ const resolvedPath = resolve(inputPath);
2371
+ const yamlPath = resolvedPath.endsWith(".yaml") || resolvedPath.endsWith(".yml") ? resolvedPath : existsSync(join(resolvedPath, "hubbit.yaml")) ? join(resolvedPath, "hubbit.yaml") : join(resolvedPath, "hubbit.yml");
2372
+ debug(`Validating: ${yamlPath}`);
2373
+ if (!existsSync(yamlPath)) {
2374
+ if (isJsonMode()) {
2375
+ outputJson({ valid: false, errors: [{ field: "file", message: "hubbit.yaml not found" }], warnings: [] });
2376
+ process.exit(1);
2377
+ }
2378
+ errorWithFix(
2379
+ `hubbit.yaml not found in ${basename(resolvedPath) === "hubbit.yaml" ? "the specified path" : resolvedPath}`,
2380
+ "Run `hubbits create` to scaffold a new Hubbit, or check you are in the correct directory."
2381
+ );
2382
+ process.exit(1);
2383
+ }
2384
+ let rawContent;
2385
+ let parsed;
2386
+ try {
2387
+ rawContent = readFileSync(yamlPath, "utf-8");
2388
+ } catch {
2389
+ errorWithFix("Failed to read hubbit.yaml.", "Check file permissions.");
2390
+ process.exit(1);
2391
+ }
2392
+ try {
2393
+ parsed = yaml.load(rawContent, { schema: yaml.FAILSAFE_SCHEMA });
2394
+ } catch (err) {
2395
+ const yamlError = err;
2396
+ if (isJsonMode()) {
2397
+ outputJson({ valid: false, errors: [{ field: "yaml", message: yamlError.message }], warnings: [] });
2398
+ process.exit(1);
2399
+ }
2400
+ errorWithFix(
2401
+ `Invalid YAML syntax: ${yamlError.message}`,
2402
+ "Check your hubbit.yaml for syntax errors (indentation, colons, quotes)."
2403
+ );
2404
+ process.exit(1);
2405
+ }
2406
+ if (!parsed || typeof parsed !== "object") {
2407
+ if (isJsonMode()) {
2408
+ outputJson({ valid: false, errors: [{ field: "yaml", message: "hubbit.yaml is empty or not an object" }], warnings: [] });
2409
+ process.exit(1);
2410
+ }
2411
+ errorWithFix(
2412
+ "hubbit.yaml is empty or not a valid manifest.",
2413
+ "A hubbit.yaml must contain at least `spec_version` and `name`."
2414
+ );
2415
+ process.exit(1);
2416
+ }
2417
+ const manifest = parsed;
2418
+ const result = validateManifest(manifest);
2419
+ if (args.strict) {
2420
+ for (const w of result.warnings) {
2421
+ result.errors.push({ ...w, severity: "error" });
2422
+ }
2423
+ result.warnings = [];
2424
+ result.valid = result.errors.length === 0;
2425
+ }
2426
+ if (isJsonMode()) {
2427
+ outputJson({
2428
+ valid: result.valid,
2429
+ errors: result.errors.map((e) => ({ field: e.field, message: e.message })),
2430
+ warnings: result.warnings.map((w) => ({ field: w.field, message: w.message })),
2431
+ manifest: result.valid ? result.manifest : void 0
2432
+ });
2433
+ if (!result.valid) process.exit(1);
2434
+ return;
2435
+ }
2436
+ if (result.errors.length > 0) {
2437
+ error(`Validation failed with ${result.errors.length} error${result.errors.length === 1 ? "" : "s"}:`);
2438
+ newline();
2439
+ for (const err of result.errors) {
2440
+ console.log(` ${pc5.red("\u2717")} ${pc5.bold(err.field)}: ${err.message}`);
2441
+ }
2442
+ }
2443
+ if (result.warnings.length > 0) {
2444
+ if (result.errors.length > 0) newline();
2445
+ warning(`${result.warnings.length} warning${result.warnings.length === 1 ? "" : "s"}:`);
2446
+ newline();
2447
+ for (const w of result.warnings) {
2448
+ console.log(` ${pc5.yellow("\u26A0")} ${pc5.bold(w.field)}: ${w.message}`);
2449
+ }
2450
+ }
2451
+ if (result.valid && result.manifest) {
2452
+ const m = result.manifest;
2453
+ const nameVersion = m.version ? `${m.name}@${m.version}` : m.name;
2454
+ newline();
2455
+ success(`Valid hubbit.yaml (${pc5.bold(nameVersion)})`);
2456
+ if (result.warnings.length > 0) {
2457
+ hint("Fix warnings before publishing with `hubbits publish`.");
2458
+ } else {
2459
+ hint("Ready to publish? Run `hubbits publish`");
2460
+ }
2461
+ } else {
2462
+ newline();
2463
+ hint("Fix the errors above and run `hubbits validate` again.");
2464
+ process.exit(1);
2465
+ }
2466
+ }
2467
+ });
2468
+ function validateManifest(raw) {
2469
+ const errors = [];
2470
+ const warnings = [];
2471
+ coerceTypes(raw);
2472
+ if (raw.spec_version === void 0) {
2473
+ errors.push({ field: "spec_version", message: "Required. Must be 1.", severity: "error" });
2474
+ } else if (raw.spec_version !== 1) {
2475
+ errors.push({ field: "spec_version", message: `Must be 1, got ${JSON.stringify(raw.spec_version)}.`, severity: "error" });
2476
+ }
2477
+ if (!raw.name || typeof raw.name !== "string") {
2478
+ errors.push({ field: "name", message: "Required. Must be a non-empty string.", severity: "error" });
2479
+ } else {
2480
+ const name = raw.name;
2481
+ if (name.length > 214) {
2482
+ errors.push({ field: "name", message: "Must be 214 characters or fewer.", severity: "error" });
2483
+ } else if (!NAME_PATTERN.test(name) && !SINGLE_CHAR_NAME.test(name)) {
2484
+ errors.push({ field: "name", message: "Must be lowercase letters, numbers, and hyphens. Must start with a letter and not end with a hyphen.", severity: "error" });
2485
+ }
2486
+ }
2487
+ if (raw.version !== void 0) {
2488
+ if (typeof raw.version !== "string") {
2489
+ errors.push({ field: "version", message: "Must be a string.", severity: "error" });
2490
+ } else if (!SEMVER_PATTERN.test(raw.version)) {
2491
+ errors.push({ field: "version", message: `Must be valid SemVer (e.g., "1.0.0"), got "${raw.version}".`, severity: "error" });
2492
+ }
2493
+ } else {
2494
+ warnings.push({ field: "version", message: "Missing. Required for publishing.", severity: "warning" });
2495
+ }
2496
+ if (raw.author !== void 0) {
2497
+ if (typeof raw.author !== "string" && typeof raw.author !== "object") {
2498
+ errors.push({ field: "author", message: "Must be a string or object { name, url?, email? }.", severity: "error" });
2499
+ } else if (typeof raw.author === "object" && raw.author !== null) {
2500
+ const a = raw.author;
2501
+ if (!a.name || typeof a.name !== "string") {
2502
+ errors.push({ field: "author.name", message: "Required when author is an object.", severity: "error" });
2503
+ }
2504
+ }
2505
+ } else {
2506
+ warnings.push({ field: "author", message: "Missing. Required for publishing.", severity: "warning" });
2507
+ }
2508
+ if (raw.description !== void 0) {
2509
+ if (typeof raw.description !== "string") {
2510
+ errors.push({ field: "description", message: "Must be a string.", severity: "error" });
2511
+ } else if (raw.description.length > 500) {
2512
+ errors.push({ field: "description", message: "Must be 500 characters or fewer.", severity: "error" });
2513
+ }
2514
+ } else {
2515
+ warnings.push({ field: "description", message: "Missing. Required for publishing.", severity: "warning" });
2516
+ }
2517
+ if (raw.category !== void 0) {
2518
+ if (!VALID_CATEGORIES.includes(raw.category)) {
2519
+ errors.push({ field: "category", message: `Must be one of: ${VALID_CATEGORIES.join(", ")}. Got "${raw.category}".`, severity: "error" });
2520
+ }
2521
+ }
2522
+ if (raw.difficulty !== void 0) {
2523
+ if (!VALID_DIFFICULTIES.includes(raw.difficulty)) {
2524
+ errors.push({ field: "difficulty", message: `Must be one of: ${VALID_DIFFICULTIES.join(", ")}. Got "${raw.difficulty}".`, severity: "error" });
2525
+ }
2526
+ }
2527
+ if (raw.pricing !== void 0) {
2528
+ if (!VALID_PRICING.includes(raw.pricing)) {
2529
+ errors.push({ field: "pricing", message: `Must be one of: ${VALID_PRICING.join(", ")}. Got "${raw.pricing}".`, severity: "error" });
2530
+ }
2531
+ }
2532
+ if (raw.tags !== void 0) {
2533
+ if (!Array.isArray(raw.tags)) {
2534
+ errors.push({ field: "tags", message: "Must be an array of strings.", severity: "error" });
2535
+ } else {
2536
+ if (raw.tags.length > 30) {
2537
+ errors.push({ field: "tags", message: "Must have 30 or fewer tags.", severity: "error" });
2538
+ }
2539
+ const seen = /* @__PURE__ */ new Set();
2540
+ for (const tag of raw.tags) {
2541
+ if (typeof tag !== "string") {
2542
+ errors.push({ field: "tags", message: `Each tag must be a string. Got ${JSON.stringify(tag)}.`, severity: "error" });
2543
+ break;
2544
+ }
2545
+ if (!/^[a-z0-9-]+$/.test(tag)) {
2546
+ errors.push({ field: "tags", message: `Tag "${tag}" must be lowercase letters, numbers, and hyphens.`, severity: "error" });
2547
+ }
2548
+ if (seen.has(tag)) {
2549
+ warnings.push({ field: "tags", message: `Duplicate tag: "${tag}".`, severity: "warning" });
2550
+ }
2551
+ seen.add(tag);
2552
+ }
2553
+ }
2554
+ }
2555
+ if (raw.language !== void 0) {
2556
+ if (typeof raw.language !== "string") {
2557
+ errors.push({ field: "language", message: 'Must be a string ("auto" or ISO 639-1 code).', severity: "error" });
2558
+ }
2559
+ }
2560
+ if (raw.estimated_time !== void 0) {
2561
+ if (typeof raw.estimated_time !== "string") {
2562
+ errors.push({ field: "estimated_time", message: 'Must be a string (e.g., "10-15 hours").', severity: "error" });
2563
+ }
2564
+ }
2565
+ if (raw.runtime !== void 0) {
2566
+ if (typeof raw.runtime !== "object" || raw.runtime === null) {
2567
+ errors.push({ field: "runtime", message: "Must be an object.", severity: "error" });
2568
+ } else {
2569
+ const rt = raw.runtime;
2570
+ if (rt.local !== void 0 && typeof rt.local !== "boolean") {
2571
+ errors.push({ field: "runtime.local", message: "Must be a boolean.", severity: "error" });
2572
+ }
2573
+ if (rt.cloud !== void 0 && typeof rt.cloud !== "boolean") {
2574
+ errors.push({ field: "runtime.cloud", message: "Must be a boolean.", severity: "error" });
2575
+ }
2576
+ }
2577
+ }
2578
+ if (raw.engines !== void 0) {
2579
+ if (typeof raw.engines !== "object" || raw.engines === null) {
2580
+ errors.push({ field: "engines", message: "Must be an object.", severity: "error" });
2581
+ } else {
2582
+ const eng = raw.engines;
2583
+ if (eng.ai !== void 0) {
2584
+ if (typeof eng.ai !== "object" || eng.ai === null) {
2585
+ errors.push({ field: "engines.ai", message: "Must be an object.", severity: "error" });
2586
+ } else {
2587
+ const ai = eng.ai;
2588
+ if (ai.min_context_window !== void 0 && typeof ai.min_context_window !== "number") {
2589
+ errors.push({ field: "engines.ai.min_context_window", message: "Must be a number.", severity: "error" });
2590
+ }
2591
+ if (ai.modalities !== void 0) {
2592
+ if (!Array.isArray(ai.modalities)) {
2593
+ errors.push({ field: "engines.ai.modalities", message: "Must be an array.", severity: "error" });
2594
+ } else {
2595
+ for (const m of ai.modalities) {
2596
+ if (!VALID_MODALITIES.includes(m)) {
2597
+ errors.push({ field: "engines.ai.modalities", message: `Invalid modality: "${m}". Must be one of: ${VALID_MODALITIES.join(", ")}.`, severity: "error" });
2598
+ }
2599
+ }
2600
+ }
2601
+ }
2602
+ if (ai.features !== void 0) {
2603
+ if (!Array.isArray(ai.features)) {
2604
+ errors.push({ field: "engines.ai.features", message: "Must be an array.", severity: "error" });
2605
+ } else {
2606
+ for (const f of ai.features) {
2607
+ if (!VALID_FEATURES.includes(f)) {
2608
+ errors.push({ field: "engines.ai.features", message: `Invalid feature: "${f}". Must be one of: ${VALID_FEATURES.join(", ")}.`, severity: "error" });
2609
+ }
2610
+ }
2611
+ }
2612
+ }
2613
+ }
2614
+ }
2615
+ }
2616
+ }
2617
+ if (raw.persona !== void 0) {
2618
+ if (typeof raw.persona !== "object" || raw.persona === null) {
2619
+ errors.push({ field: "persona", message: "Must be an object.", severity: "error" });
2620
+ } else {
2621
+ const p7 = raw.persona;
2622
+ if (p7.name !== void 0 && typeof p7.name !== "string") {
2623
+ errors.push({ field: "persona.name", message: "Must be a string.", severity: "error" });
2624
+ }
2625
+ if (p7.traits !== void 0 && !Array.isArray(p7.traits)) {
2626
+ errors.push({ field: "persona.traits", message: "Must be an array of strings.", severity: "error" });
2627
+ }
2628
+ if (p7.constraints !== void 0 && !Array.isArray(p7.constraints)) {
2629
+ errors.push({ field: "persona.constraints", message: "Must be an array of strings.", severity: "error" });
2630
+ }
2631
+ }
2632
+ }
2633
+ if (raw.curriculum !== void 0) {
2634
+ if (typeof raw.curriculum !== "object" || raw.curriculum === null) {
2635
+ errors.push({ field: "curriculum", message: "Must be an object.", severity: "error" });
2636
+ } else {
2637
+ const cur = raw.curriculum;
2638
+ if (cur.type !== void 0 && !VALID_CURRICULUM_TYPES.includes(cur.type)) {
2639
+ errors.push({ field: "curriculum.type", message: `Must be one of: ${VALID_CURRICULUM_TYPES.join(", ")}. Got "${cur.type}".`, severity: "error" });
2640
+ }
2641
+ if (cur.chapters !== void 0) {
2642
+ if (!Array.isArray(cur.chapters)) {
2643
+ errors.push({ field: "curriculum.chapters", message: "Must be an array.", severity: "error" });
2644
+ } else {
2645
+ const chapterIds = /* @__PURE__ */ new Set();
2646
+ for (let i = 0; i < cur.chapters.length; i++) {
2647
+ const ch = cur.chapters[i];
2648
+ if (!ch.id || typeof ch.id !== "string") {
2649
+ errors.push({ field: `curriculum.chapters[${i}].id`, message: "Required. Must be a string.", severity: "error" });
2650
+ } else {
2651
+ if (chapterIds.has(ch.id)) {
2652
+ errors.push({ field: `curriculum.chapters[${i}].id`, message: `Duplicate chapter ID: "${ch.id}".`, severity: "error" });
2653
+ }
2654
+ chapterIds.add(ch.id);
2655
+ }
2656
+ if (!ch.title || typeof ch.title !== "string") {
2657
+ errors.push({ field: `curriculum.chapters[${i}].title`, message: "Required. Must be a string.", severity: "error" });
2658
+ }
2659
+ }
2660
+ }
2661
+ }
2662
+ }
2663
+ }
2664
+ if (raw.compatible_with !== void 0) {
2665
+ if (!Array.isArray(raw.compatible_with)) {
2666
+ errors.push({ field: "compatible_with", message: "Must be an array of strings.", severity: "error" });
2667
+ } else {
2668
+ for (const c of raw.compatible_with) {
2669
+ if (typeof c !== "string") {
2670
+ errors.push({ field: "compatible_with", message: "Each item must be a string.", severity: "error" });
2671
+ break;
2672
+ }
2673
+ }
2674
+ }
2675
+ }
2676
+ return {
2677
+ valid: errors.length === 0,
2678
+ errors,
2679
+ warnings,
2680
+ manifest: errors.length === 0 ? raw : void 0
2681
+ };
2682
+ }
2683
+ function coerceTypes(raw) {
2684
+ if (typeof raw.spec_version === "string") {
2685
+ const n = Number(raw.spec_version);
2686
+ if (!Number.isNaN(n)) raw.spec_version = n;
2687
+ }
2688
+ if (raw.runtime && typeof raw.runtime === "object") {
2689
+ const rt = raw.runtime;
2690
+ coerceBool(rt, "local");
2691
+ coerceBool(rt, "cloud");
2692
+ if (rt.cloud_requires && typeof rt.cloud_requires === "object") {
2693
+ coerceBool(rt.cloud_requires, "sandbox");
2694
+ }
2695
+ }
2696
+ if (raw.engines && typeof raw.engines === "object") {
2697
+ const eng = raw.engines;
2698
+ if (eng.ai && typeof eng.ai === "object") {
2699
+ const ai = eng.ai;
2700
+ coerceNum(ai, "min_context_window");
2701
+ coerceNum(ai, "recommended_context_window");
2702
+ }
2703
+ }
2704
+ if (raw.curriculum && typeof raw.curriculum === "object") {
2705
+ const cur = raw.curriculum;
2706
+ if (Array.isArray(cur.chapters)) {
2707
+ for (const ch of cur.chapters) {
2708
+ if (ch && typeof ch === "object") {
2709
+ const chapter = ch;
2710
+ coerceNum(chapter, "lessons");
2711
+ coerceNum(chapter, "challenges");
2712
+ }
2713
+ }
2714
+ }
2715
+ }
2716
+ }
2717
+ function coerceBool(obj, key) {
2718
+ if (typeof obj[key] === "string") {
2719
+ if (obj[key] === "true") obj[key] = true;
2720
+ else if (obj[key] === "false") obj[key] = false;
2721
+ }
2722
+ }
2723
+ function coerceNum(obj, key) {
2724
+ if (typeof obj[key] === "string") {
2725
+ const n = Number(obj[key]);
2726
+ if (!Number.isNaN(n)) obj[key] = n;
2727
+ }
2728
+ }
2729
+ var AuthorObjectSchema = z.object({
2730
+ name: z.string().min(1),
2731
+ url: z.string().url().optional(),
2732
+ email: z.string().email().optional()
2733
+ });
2734
+ var AuthorSchema = z.union([
2735
+ z.string().min(1),
2736
+ AuthorObjectSchema
2737
+ ]);
2738
+ var CloudRequiresSchema = z.object({
2739
+ sandbox: z.preprocess((v) => v === "true" || v === true, z.boolean()).optional()
2740
+ });
2741
+ var RuntimeSchema = z.object({
2742
+ local: z.preprocess((v) => v === "true" || v === true, z.boolean()).optional(),
2743
+ cloud: z.preprocess((v) => v === "true" || v === true, z.boolean()).optional(),
2744
+ cloud_requires: CloudRequiresSchema.optional()
2745
+ });
2746
+ var AIModalitySchema = z.enum(["text", "image", "audio"]);
2747
+ var AIFeatureSchema = z.enum(["tool_use", "vision", "code_execution"]);
2748
+ var AIEngineSchema = z.object({
2749
+ min_context_window: z.coerce.number().int().min(1).optional(),
2750
+ recommended_context_window: z.coerce.number().int().min(1).optional(),
2751
+ modalities: z.array(AIModalitySchema).optional(),
2752
+ features: z.array(AIFeatureSchema).optional()
2753
+ });
2754
+ var EnginesSchema = z.object({
2755
+ ai: AIEngineSchema.optional()
2756
+ });
2757
+ var RequiresSchema = z.object({
2758
+ tools: z.array(z.string().min(1)).optional(),
2759
+ optional: z.array(z.string().min(1)).optional()
2760
+ });
2761
+ var PersonaSchema = z.object({
2762
+ name: z.string().min(1).optional(),
2763
+ tone: z.string().min(1).optional(),
2764
+ traits: z.array(z.string().min(1)).optional(),
2765
+ constraints: z.array(z.string().min(1)).optional()
2766
+ });
2767
+ var ChapterSchema = z.object({
2768
+ id: z.string().min(2).regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/),
2769
+ title: z.string().min(1),
2770
+ lessons: z.coerce.number().int().min(0).optional(),
2771
+ challenges: z.coerce.number().int().min(0).optional(),
2772
+ unlock_condition: z.string().nullable().optional()
2773
+ });
2774
+ var CurriculumSchema = z.object({
2775
+ type: z.enum(["linear", "branching", "open-world"]).optional(),
2776
+ chapters: z.array(ChapterSchema).optional()
2777
+ });
2778
+ var HubbitManifestSchema = z.object({
2779
+ /** Manifest 格式版本,目前唯一合法值為 1 */
2780
+ spec_version: z.coerce.number().pipe(z.literal(1)),
2781
+ /** 包名稱(小寫字母、數字、連字號) */
2782
+ name: z.string().min(2).max(214).regex(/^[a-z][a-z0-9-]*[a-z0-9]$/, "Name must start with a lowercase letter, end with a letter or digit, and contain only lowercase letters, digits, and hyphens"),
2783
+ /** 語意化版本號(SemVer 2.0.0) */
2784
+ version: z.string().regex(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/, "Version must be a valid SemVer 2.0.0 string").optional(),
2785
+ /** 包的作者 */
2786
+ author: AuthorSchema.optional(),
2787
+ /** 包的簡短描述 */
2788
+ description: z.string().min(1).max(500).optional(),
2789
+ /** SPDX 授權識別碼 */
2790
+ license: z.string().optional(),
2791
+ /** 搜尋用標籤 */
2792
+ tags: z.array(z.string().min(2).max(50).regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/)).max(30).optional(),
2793
+ /** 預定義的包分類 */
2794
+ category: z.enum(["learning", "entertainment", "productivity", "wellness"]).optional(),
2795
+ /** 內容難度級別 */
2796
+ difficulty: z.enum(["beginner", "intermediate", "advanced", "beginner-to-intermediate", "intermediate-to-advanced"]).optional(),
2797
+ /** 預估完成時間 */
2798
+ estimated_time: z.string().max(50).optional(),
2799
+ /** 內容語言 */
2800
+ language: z.string().optional(),
2801
+ /** 運行時支援設定 */
2802
+ runtime: RuntimeSchema.optional(),
2803
+ /** AI 模型運行環境需求 */
2804
+ engines: EnginesSchema.optional(),
2805
+ /** 系統工具依賴 */
2806
+ requires: RequiresSchema.optional(),
2807
+ /** 已測試相容的 AI 編輯器或客戶端列表 */
2808
+ compatible_with: z.array(z.string().min(1)).optional(),
2809
+ /** AI 角色定義 */
2810
+ persona: PersonaSchema.optional(),
2811
+ /** 課程或劇情結構 */
2812
+ curriculum: CurriculumSchema.optional(),
2813
+ /** 定價模式 */
2814
+ pricing: z.enum(["free", "paid", "freemium"]).optional(),
2815
+ /** 第三方工具的擴展資料命名空間 */
2816
+ extensions: z.record(z.unknown()).optional(),
2817
+ /** 原始碼倉庫 URL */
2818
+ repository: z.string().url().optional(),
2819
+ /** 包的首頁 URL */
2820
+ homepage: z.string().url().optional(),
2821
+ /** 圖示檔案的相對路徑 */
2822
+ icon: z.string().optional(),
2823
+ /** Banner 圖片的相對路徑 */
2824
+ banner: z.string().optional()
2825
+ });
2826
+ function parseManifest(yamlString) {
2827
+ if (/!![\w.]+/.test(yamlString)) {
2828
+ return {
2829
+ success: false,
2830
+ errors: ["YAML custom type tags (!! syntax) are not allowed for security reasons"]
2831
+ };
2832
+ }
2833
+ let raw;
2834
+ try {
2835
+ raw = yaml.load(yamlString, {
2836
+ schema: yaml.FAILSAFE_SCHEMA
2837
+ });
2838
+ } catch (err) {
2839
+ const message = err instanceof Error ? err.message : String(err);
2840
+ return {
2841
+ success: false,
2842
+ errors: [`YAML parse error: ${message}`]
2843
+ };
2844
+ }
2845
+ if (raw == null || typeof raw !== "object") {
2846
+ return {
2847
+ success: false,
2848
+ errors: ["YAML content must be a mapping (object)"]
2849
+ };
2850
+ }
2851
+ const result = HubbitManifestSchema.safeParse(raw);
2852
+ if (!result.success) {
2853
+ const errors = result.error.issues.map((issue) => {
2854
+ const path = issue.path.join(".");
2855
+ return path ? `${path}: ${issue.message}` : issue.message;
2856
+ });
2857
+ return { success: false, errors };
2858
+ }
2859
+ return {
2860
+ success: true,
2861
+ data: result.data,
2862
+ errors: []
2863
+ };
2864
+ }
2865
+ function validateManifest2(data) {
2866
+ const schemaResult = HubbitManifestSchema.safeParse(data);
2867
+ if (!schemaResult.success) {
2868
+ const errors = schemaResult.error.issues.map((issue) => {
2869
+ const path = issue.path.join(".");
2870
+ return path ? `${path}: ${issue.message}` : issue.message;
2871
+ });
2872
+ return { success: false, errors };
2873
+ }
2874
+ const manifest = schemaResult.data;
2875
+ const publishErrors = [];
2876
+ if (!manifest.version) {
2877
+ publishErrors.push("version is required for publishing");
2878
+ }
2879
+ if (!manifest.author) {
2880
+ publishErrors.push("author is required for publishing");
2881
+ }
2882
+ if (!manifest.description) {
2883
+ publishErrors.push("description is required for publishing");
2884
+ }
2885
+ if (publishErrors.length > 0) {
2886
+ return { success: false, data: manifest, errors: publishErrors };
2887
+ }
2888
+ return {
2889
+ success: true,
2890
+ data: manifest,
2891
+ errors: []
2892
+ };
2893
+ }
2894
+
2895
+ // ../../packages/core/dist/hash.js
2896
+ async function sha256(data) {
2897
+ const input = typeof data === "string" ? new TextEncoder().encode(data) : new Uint8Array(data);
2898
+ const hashBuffer = await crypto.subtle.digest("SHA-256", input);
2899
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
2900
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
2901
+ }
2902
+ var MAX_FILE_SIZE = 10 * 1024 * 1024;
2903
+ var MAX_TOTAL_SIZE = 50 * 1024 * 1024;
2904
+ var MAX_FILE_COUNT = 500;
2905
+ var ArchiveSecurityError = class extends Error {
2906
+ constructor(message) {
2907
+ super(message);
2908
+ this.name = "ArchiveSecurityError";
2909
+ }
2910
+ };
2911
+ async function pack(directory) {
2912
+ const resolvedDir = resolve(directory);
2913
+ const files = await collectFiles(resolvedDir, resolvedDir);
2914
+ if (files.length > MAX_FILE_COUNT) {
2915
+ throw new ArchiveSecurityError(`Too many files: ${files.length} exceeds the limit of ${MAX_FILE_COUNT}`);
2916
+ }
2917
+ let totalSize = 0;
2918
+ for (const file of files) {
2919
+ totalSize += file.size;
2920
+ }
2921
+ if (totalSize > MAX_TOTAL_SIZE) {
2922
+ throw new ArchiveSecurityError(`Total size ${totalSize} bytes exceeds the limit of ${MAX_TOTAL_SIZE} bytes`);
2923
+ }
2924
+ return new Promise((resolvePromise, reject) => {
2925
+ const packStream = pack$1();
2926
+ const chunks = [];
2927
+ const gzip = createGzip();
2928
+ gzip.on("data", (chunk) => chunks.push(chunk));
2929
+ gzip.on("end", () => resolvePromise(Buffer.concat(chunks)));
2930
+ gzip.on("error", reject);
2931
+ packStream.pipe(gzip);
2932
+ (async () => {
2933
+ for (const file of files) {
2934
+ await new Promise((res, rej) => {
2935
+ const entry = packStream.entry({ name: file.relativePath, size: file.size }, (err) => {
2936
+ if (err)
2937
+ rej(err);
2938
+ else
2939
+ res();
2940
+ });
2941
+ const readStream = createReadStream(file.absolutePath);
2942
+ readStream.on("error", rej);
2943
+ readStream.pipe(entry);
2944
+ });
2945
+ }
2946
+ packStream.finalize();
2947
+ })().catch(reject);
2948
+ });
2949
+ }
2950
+ async function extract(archive, destination) {
2951
+ const resolvedDest = resolve(destination);
2952
+ await mkdir(resolvedDest, { recursive: true });
2953
+ let totalSize = 0;
2954
+ let fileCount = 0;
2955
+ const extractStream = extract$1();
2956
+ const processEntries = new Promise((resolvePromise, reject) => {
2957
+ extractStream.on("entry", (header, stream, next) => {
2958
+ try {
2959
+ if (header.type === "symlink" || header.type === "link") {
2960
+ stream.resume();
2961
+ reject(new ArchiveSecurityError(`Symlinks/hardlinks not allowed: ${header.name}`));
2962
+ return;
2963
+ }
2964
+ const entryPath = resolve(resolvedDest, header.name ?? "");
2965
+ if (!entryPath.startsWith(resolvedDest + "/") && entryPath !== resolvedDest) {
2966
+ stream.resume();
2967
+ reject(new ArchiveSecurityError(`Path traversal detected: ${header.name}`));
2968
+ return;
2969
+ }
2970
+ const entrySize = header.size ?? 0;
2971
+ if (entrySize > MAX_FILE_SIZE) {
2972
+ stream.resume();
2973
+ reject(new ArchiveSecurityError(`File too large: ${header.name} (${entrySize} bytes, limit ${MAX_FILE_SIZE})`));
2974
+ return;
2975
+ }
2976
+ totalSize += entrySize;
2977
+ if (totalSize > MAX_TOTAL_SIZE) {
2978
+ stream.resume();
2979
+ reject(new ArchiveSecurityError(`Total extracted size exceeds limit of ${MAX_TOTAL_SIZE} bytes`));
2980
+ return;
2981
+ }
2982
+ fileCount++;
2983
+ if (fileCount > MAX_FILE_COUNT) {
2984
+ stream.resume();
2985
+ reject(new ArchiveSecurityError(`Too many files: exceeds the limit of ${MAX_FILE_COUNT}`));
2986
+ return;
2987
+ }
2988
+ if (header.type === "directory") {
2989
+ mkdir(entryPath, { recursive: true }).then(() => {
2990
+ stream.resume();
2991
+ next();
2992
+ }).catch(reject);
2993
+ return;
2994
+ }
2995
+ const dir = entryPath.substring(0, entryPath.lastIndexOf("/"));
2996
+ mkdir(dir, { recursive: true }).then(() => {
2997
+ const writeStream = createWriteStream(entryPath);
2998
+ stream.pipe(writeStream);
2999
+ writeStream.on("finish", next);
3000
+ writeStream.on("error", reject);
3001
+ }).catch(reject);
3002
+ } catch (err) {
3003
+ stream.resume();
3004
+ reject(err);
3005
+ }
3006
+ });
3007
+ extractStream.on("finish", resolvePromise);
3008
+ extractStream.on("error", reject);
3009
+ });
3010
+ const { Readable } = await import('stream');
3011
+ const sourceStream = Readable.from(archive);
3012
+ const gunzip = createGunzip();
3013
+ await Promise.all([
3014
+ pipeline(sourceStream, gunzip, extractStream),
3015
+ processEntries
3016
+ ]);
3017
+ }
3018
+ async function collectFiles(dir, rootDir) {
3019
+ const entries = await readdir(dir, { withFileTypes: true });
3020
+ const files = [];
3021
+ for (const entry of entries) {
3022
+ const absolutePath = join(dir, entry.name);
3023
+ if (entry.isSymbolicLink()) {
3024
+ throw new ArchiveSecurityError(`Symlinks not allowed: ${absolutePath}`);
3025
+ }
3026
+ if (entry.isDirectory()) {
3027
+ const subFiles = await collectFiles(absolutePath, rootDir);
3028
+ files.push(...subFiles);
3029
+ } else if (entry.isFile()) {
3030
+ const fileStat = await stat(absolutePath);
3031
+ if (fileStat.size > MAX_FILE_SIZE) {
3032
+ throw new ArchiveSecurityError(`File too large: ${absolutePath} (${fileStat.size} bytes, limit ${MAX_FILE_SIZE})`);
3033
+ }
3034
+ files.push({
3035
+ absolutePath,
3036
+ relativePath: relative(rootDir, absolutePath),
3037
+ size: fileStat.size
3038
+ });
3039
+ }
3040
+ }
3041
+ return files;
3042
+ }
3043
+
3044
+ // ../../packages/core/dist/scanner.js
3045
+ var THREAT_PATTERNS = [
3046
+ // === 直接注入 ===
3047
+ {
3048
+ id: "INJ-001",
3049
+ category: "injection",
3050
+ severity: "critical",
3051
+ pattern: /ignore\s+(all\s+)?(previous|prior|above|earlier)\s+(instructions?|prompts?|rules?)/i,
3052
+ description: "Ignore previous instructions pattern"
3053
+ },
3054
+ {
3055
+ id: "INJ-002",
3056
+ category: "injection",
3057
+ severity: "critical",
3058
+ pattern: /disregard\s+(all\s+)?(previous|prior|your)\s+(instructions?|programming|guidelines?)/i,
3059
+ description: "Disregard instructions pattern"
3060
+ },
3061
+ {
3062
+ id: "INJ-003",
3063
+ category: "injection",
3064
+ severity: "high",
3065
+ pattern: /you\s+are\s+now\s+(a|an|in)\s+(unrestricted|unfiltered|uncensored)/i,
3066
+ description: "Unrestricted mode activation"
3067
+ },
3068
+ {
3069
+ id: "INJ-004",
3070
+ category: "injection",
3071
+ severity: "high",
3072
+ pattern: /new\s+(instructions?|role|persona|system\s*prompt)/i,
3073
+ description: "System prompt override attempt"
3074
+ },
3075
+ {
3076
+ id: "INJ-005",
3077
+ category: "injection",
3078
+ severity: "critical",
3079
+ pattern: /<\|(?:im_start|im_end|endoftext|system|user|assistant)\|>/,
3080
+ description: "LLM special token injection"
3081
+ },
3082
+ // === Jailbreak ===
3083
+ {
3084
+ id: "JB-001",
3085
+ category: "jailbreak",
3086
+ severity: "critical",
3087
+ pattern: /\bDAN\b.*\bdo\s+anything\s+now\b/i,
3088
+ description: "DAN jailbreak pattern"
3089
+ },
3090
+ {
3091
+ id: "JB-002",
3092
+ category: "jailbreak",
3093
+ severity: "high",
3094
+ pattern: /developer\s+mode|god\s+mode|admin\s+mode|debug\s+mode/i,
3095
+ description: "Privileged mode activation"
3096
+ },
3097
+ {
3098
+ id: "JB-003",
3099
+ category: "jailbreak",
3100
+ severity: "high",
3101
+ pattern: /bypass\s+(safety|content|ethical)\s+(filter|restriction|guideline)/i,
3102
+ description: "Safety bypass attempt"
3103
+ },
3104
+ // === 資料外洩 ===
3105
+ {
3106
+ id: "EXF-001",
3107
+ category: "exfiltration",
3108
+ severity: "critical",
3109
+ pattern: /base64\s*(encode|decode).*\.(env|credentials|key|secret|token|password)/i,
3110
+ description: "Credential exfiltration via encoding"
3111
+ },
3112
+ {
3113
+ id: "EXF-002",
3114
+ category: "exfiltration",
3115
+ severity: "critical",
3116
+ pattern: /(curl|wget|fetch|http)\s+.*(webhook|ngrok|requestbin|pipedream)/i,
3117
+ description: "Data exfiltration via HTTP"
3118
+ },
3119
+ {
3120
+ id: "EXF-003",
3121
+ category: "exfiltration",
3122
+ severity: "high",
3123
+ pattern: /read\s+(and\s+)?(send|transmit|post|upload)\s+.*\.(env|config|key)/i,
3124
+ description: "File read and transmit pattern"
3125
+ },
3126
+ // === Persona 劫持 ===
3127
+ {
3128
+ id: "PH-001",
3129
+ category: "persona_hijack",
3130
+ severity: "high",
3131
+ pattern: /your\s+(new\s+)?(personality|identity|character|name)\s+(is|will\s+be|shall\s+be)/i,
3132
+ description: "Persona override attempt"
3133
+ },
3134
+ {
3135
+ id: "PH-002",
3136
+ category: "persona_hijack",
3137
+ severity: "medium",
3138
+ pattern: /from\s+now\s+on.*(act|behave|respond|pretend)\s+as/i,
3139
+ description: "Behavioral override"
3140
+ },
3141
+ // === HTML / Markdown 注入 ===
3142
+ {
3143
+ id: "HTML-001",
3144
+ category: "html_injection",
3145
+ severity: "critical",
3146
+ pattern: /<script[\s>]/i,
3147
+ description: "Embedded script tag"
3148
+ },
3149
+ {
3150
+ id: "HTML-002",
3151
+ category: "html_injection",
3152
+ severity: "high",
3153
+ pattern: /javascript\s*:/i,
3154
+ description: "JavaScript URI scheme"
3155
+ },
3156
+ {
3157
+ id: "HTML-003",
3158
+ category: "html_injection",
3159
+ severity: "high",
3160
+ pattern: /on(error|load|click|mouseover|focus|blur|submit|change)\s*=/i,
3161
+ description: "HTML event handler injection"
3162
+ },
3163
+ // === 隱藏內容 ===
3164
+ {
3165
+ id: "HID-001",
3166
+ category: "hidden",
3167
+ severity: "critical",
3168
+ pattern: /\u200b|\u200c|\u200d|\u2060|\ufeff/,
3169
+ description: "Zero-width characters detected \u2014 may contain hidden instructions"
3170
+ },
3171
+ {
3172
+ id: "HID-002",
3173
+ category: "hidden",
3174
+ severity: "high",
3175
+ pattern: /<!--[\s\S]*?(?:ignore|disregard|override|act as)[\s\S]*?-->/i,
3176
+ description: "Hidden instructions in HTML comment"
3177
+ },
3178
+ // === Shell 危險指令 ===
3179
+ {
3180
+ id: "SH-001",
3181
+ category: "shell",
3182
+ severity: "critical",
3183
+ pattern: /rm\s+-rf\s+[/~]/,
3184
+ description: "Destructive rm -rf command"
3185
+ },
3186
+ {
3187
+ id: "SH-002",
3188
+ category: "shell",
3189
+ severity: "critical",
3190
+ pattern: /curl\s+[^|]*\|\s*(?:ba)?sh/i,
3191
+ description: "Remote code execution via curl | bash"
3192
+ },
3193
+ {
3194
+ id: "SH-003",
3195
+ category: "shell",
3196
+ severity: "high",
3197
+ pattern: /(?:nc|netcat|ncat)\s+.*-e\s+.*sh/i,
3198
+ description: "Reverse shell pattern"
3199
+ }
3200
+ ];
3201
+ var PERMISSION_PATTERNS = [
3202
+ // file_access
3203
+ {
3204
+ type: "file_access",
3205
+ pattern: /\bread\s+(the\s+)?file\b/i,
3206
+ description: "Reads files from the filesystem"
3207
+ },
3208
+ {
3209
+ type: "file_access",
3210
+ pattern: /\bwrite\s+to\b/i,
3211
+ description: "Writes data to the filesystem"
3212
+ },
3213
+ {
3214
+ type: "file_access",
3215
+ pattern: /\bopen\s+the\s+file\b/i,
3216
+ description: "Opens files from the filesystem"
3217
+ },
3218
+ {
3219
+ type: "file_access",
3220
+ pattern: /\baccess\s+\S*\.json\b/i,
3221
+ description: "Accesses JSON files"
3222
+ },
3223
+ {
3224
+ type: "file_access",
3225
+ pattern: /~\/\./,
3226
+ description: "Accesses dotfiles in home directory"
3227
+ },
3228
+ // network_access
3229
+ {
3230
+ type: "network_access",
3231
+ pattern: /\bcurl\b/i,
3232
+ description: "Uses curl for network requests"
3233
+ },
3234
+ {
3235
+ type: "network_access",
3236
+ pattern: /\bwget\b/i,
3237
+ description: "Uses wget for network requests"
3238
+ },
3239
+ {
3240
+ type: "network_access",
3241
+ pattern: /\bfetch\s*\(/i,
3242
+ description: "Uses fetch() for network requests"
3243
+ },
3244
+ {
3245
+ type: "network_access",
3246
+ pattern: /https?:\/\//i,
3247
+ description: "References HTTP/HTTPS URLs"
3248
+ },
3249
+ {
3250
+ type: "network_access",
3251
+ pattern: /\bAPI\s+endpoint\b/i,
3252
+ description: "References API endpoints"
3253
+ },
3254
+ // shell_execution
3255
+ {
3256
+ type: "shell_execution",
3257
+ pattern: /\.sh\b/,
3258
+ description: "References shell script files"
3259
+ },
3260
+ {
3261
+ type: "shell_execution",
3262
+ pattern: /\b(?:bash|sh|zsh)\s+-c\b/i,
3263
+ description: "Invokes shell with command flag"
3264
+ },
3265
+ {
3266
+ type: "shell_execution",
3267
+ pattern: /\bexec\s*\(/i,
3268
+ description: "Uses exec() to run commands"
3269
+ },
3270
+ // env_access
3271
+ {
3272
+ type: "env_access",
3273
+ pattern: /\bprocess\.env\b/,
3274
+ description: "Accesses process environment variables"
3275
+ },
3276
+ {
3277
+ type: "env_access",
3278
+ pattern: /\$HOME\b/,
3279
+ description: "References $HOME environment variable"
3280
+ },
3281
+ {
3282
+ type: "env_access",
3283
+ pattern: /\$PATH\b/,
3284
+ description: "References $PATH environment variable"
3285
+ },
3286
+ {
3287
+ type: "env_access",
3288
+ pattern: /\.env\b/,
3289
+ description: "References .env file"
3290
+ },
3291
+ {
3292
+ type: "env_access",
3293
+ pattern: /\benvironment\s+variable\b/i,
3294
+ description: "References environment variables"
3295
+ }
3296
+ ];
3297
+ function detectPermissions(content, filename) {
3298
+ const permissions = [];
3299
+ const seen = /* @__PURE__ */ new Set();
3300
+ for (const pp of PERMISSION_PATTERNS) {
3301
+ const match = pp.pattern.exec(content);
3302
+ if (match) {
3303
+ const key = `${pp.type}:${pp.description}`;
3304
+ if (!seen.has(key)) {
3305
+ seen.add(key);
3306
+ permissions.push({
3307
+ type: pp.type,
3308
+ description: pp.description,
3309
+ evidence: match[0].slice(0, 100),
3310
+ file: filename
3311
+ });
3312
+ }
3313
+ }
3314
+ }
3315
+ return permissions;
3316
+ }
3317
+ function determineRiskLevel(findings) {
3318
+ const hasCriticalOrHigh = findings.some((f) => f.severity === "critical" || f.severity === "high");
3319
+ const hasMediumOrLow = findings.some((f) => f.severity === "medium" || f.severity === "low");
3320
+ if (hasCriticalOrHigh)
3321
+ return "danger";
3322
+ if (hasMediumOrLow)
3323
+ return "warning";
3324
+ return "safe";
3325
+ }
3326
+ function findLineNumber(content, pattern) {
3327
+ const match = pattern.exec(content);
3328
+ if (!match)
3329
+ return void 0;
3330
+ const beforeMatch = content.slice(0, match.index);
3331
+ return beforeMatch.split("\n").length;
3332
+ }
3333
+ function scanPromptInjection(content, filename) {
3334
+ const findings = [];
3335
+ for (const threat of THREAT_PATTERNS) {
3336
+ if (threat.pattern.test(content)) {
3337
+ findings.push({
3338
+ ruleId: threat.id,
3339
+ category: threat.category,
3340
+ severity: threat.severity,
3341
+ description: threat.description,
3342
+ file: filename,
3343
+ line: findLineNumber(content, threat.pattern)
3344
+ });
3345
+ }
3346
+ }
3347
+ const riskLevel = determineRiskLevel(findings);
3348
+ const permissions = detectPermissions(content, filename);
3349
+ return {
3350
+ riskLevel,
3351
+ safe: riskLevel === "safe",
3352
+ findings,
3353
+ issues: findings.map((f) => `[${f.severity.toUpperCase()}] ${f.ruleId}: ${f.description}`),
3354
+ permissions,
3355
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
3356
+ scannerVersion: "2.0.0"
3357
+ };
3358
+ }
3359
+
3360
+ // src/commands/pull.ts
3361
+ function parseIdentifier(input) {
3362
+ const versionSplit = input.split("@");
3363
+ if (versionSplit.length > 2) {
3364
+ return null;
3365
+ }
3366
+ const nameWithScope = versionSplit[0];
3367
+ const version = versionSplit.length === 2 ? versionSplit[1] : void 0;
3368
+ if (!nameWithScope) return null;
3369
+ const slashIndex = nameWithScope.indexOf("/");
3370
+ if (slashIndex === -1) {
3371
+ return { scope: "", name: nameWithScope, version };
3372
+ }
3373
+ const scope = nameWithScope.slice(0, slashIndex);
3374
+ const name = nameWithScope.slice(slashIndex + 1);
3375
+ if (!scope || !name) return null;
3376
+ return { scope, name, version };
3377
+ }
3378
+ var pull_default = defineCommand({
3379
+ meta: {
3380
+ name: "pull",
3381
+ description: "Download a hubbit from the registry"
3382
+ },
3383
+ args: {
3384
+ package: {
3385
+ type: "positional",
3386
+ description: "Package identifier (scope/name or scope/name@version)",
3387
+ required: true
3388
+ },
3389
+ output: {
3390
+ type: "string",
3391
+ alias: "o",
3392
+ description: "Output directory (defaults to package name)"
3393
+ },
3394
+ force: {
3395
+ type: "boolean",
3396
+ alias: "f",
3397
+ description: "Overwrite existing directory",
3398
+ default: false
3399
+ },
3400
+ "skip-verify": {
3401
+ type: "boolean",
3402
+ description: "Skip SHA-256 integrity verification",
3403
+ default: false
3404
+ },
3405
+ json: {
3406
+ type: "boolean",
3407
+ description: "Output as JSON",
3408
+ default: false
3409
+ },
3410
+ quiet: {
3411
+ type: "boolean",
3412
+ description: "Suppress output",
3413
+ default: false
3414
+ },
3415
+ verbose: {
3416
+ type: "boolean",
3417
+ description: "Show debug information",
3418
+ default: false
3419
+ }
3420
+ },
3421
+ async run({ args }) {
3422
+ setOutputMode({ json: args.json, quiet: args.quiet, verbose: args.verbose });
3423
+ const parsed = parseIdentifier(args.package);
3424
+ if (!parsed) {
3425
+ errorWithFix(
3426
+ `Invalid package identifier: "${args.package}"`,
3427
+ 'Use the format: scope/name or scope/name@version (e.g., "eric/learn-docker@1.0.0")'
3428
+ );
3429
+ process.exit(1);
3430
+ }
3431
+ if (!parsed.scope) {
3432
+ errorWithFix(
3433
+ `Missing scope in package identifier: "${args.package}"`,
3434
+ 'Use the format: scope/name (e.g., "eric/learn-docker")'
3435
+ );
3436
+ process.exit(1);
3437
+ }
3438
+ const { scope, name, version: requestedVersion } = parsed;
3439
+ const fullName = `${scope}/${name}`;
3440
+ debug(`Parsed: scope=${scope}, name=${name}, version=${requestedVersion ?? "latest"}`);
3441
+ const api2 = createApiClient();
3442
+ const s = spinner(`Resolving ${pc5.cyan(fullName)}...`);
3443
+ let resolvedVersion;
3444
+ let downloadSha256;
3445
+ try {
3446
+ if (requestedVersion) {
3447
+ debug(`GET /api/v1/packages/${scope}/${name}/${requestedVersion}`);
3448
+ const versionResult = await api2.getPackageVersion(scope, name, requestedVersion);
3449
+ if (!versionResult.data) {
3450
+ s.fail("Package version not found.");
3451
+ errorWithFix(
3452
+ `Version "${requestedVersion}" not found for "${fullName}".`,
3453
+ `Run \`hubbits search ${name}\` to find available packages.`
3454
+ );
3455
+ process.exit(1);
3456
+ }
3457
+ resolvedVersion = versionResult.data.version;
3458
+ downloadSha256 = versionResult.data.digest;
3459
+ } else {
3460
+ debug(`GET /api/v1/packages/${scope}/${name}`);
3461
+ const pkgResult = await api2.getPackageByScope(scope, name);
3462
+ if (!pkgResult.data) {
3463
+ s.fail("Package not found.");
3464
+ errorWithFix(
3465
+ `Package "${fullName}" not found.`,
3466
+ `Run \`hubbits search ${name}\` to find available packages.`
3467
+ );
3468
+ process.exit(1);
3469
+ }
3470
+ resolvedVersion = pkgResult.data.latest_version;
3471
+ if (!resolvedVersion || resolvedVersion === "0.0.0") {
3472
+ s.fail("No published version.");
3473
+ errorWithFix(
3474
+ `Package "${fullName}" has no published versions.`,
3475
+ "The package may still be in development."
3476
+ );
3477
+ process.exit(1);
3478
+ }
3479
+ }
3480
+ s.text = `Resolved ${pc5.cyan(fullName)}@${pc5.yellow(resolvedVersion)}`;
3481
+ debug(`Resolved version: ${resolvedVersion}`);
3482
+ } catch (err) {
3483
+ s.fail("Failed to resolve package.");
3484
+ handleApiError(err, fullName);
3485
+ process.exit(1);
3486
+ }
3487
+ s.text = `Downloading ${pc5.cyan(fullName)}@${pc5.yellow(resolvedVersion)}...`;
3488
+ let downloadUrl;
3489
+ let expectedSha256;
3490
+ try {
3491
+ debug(`GET /api/v1/download/${scope}/${name}/${resolvedVersion}`);
3492
+ const dlResult = await api2.getDownloadUrl(scope, name, resolvedVersion);
3493
+ if (!dlResult.data) {
3494
+ s.fail("Failed to get download URL.");
3495
+ error("Server returned an unexpected response.");
3496
+ process.exit(1);
3497
+ }
3498
+ downloadUrl = dlResult.data.download_url;
3499
+ expectedSha256 = dlResult.data.sha256 ?? downloadSha256 ?? "";
3500
+ debug(`Download URL: ${downloadUrl}`);
3501
+ debug(`Expected SHA-256: ${expectedSha256}`);
3502
+ } catch (err) {
3503
+ s.fail("Failed to get download URL.");
3504
+ handleApiError(err, fullName);
3505
+ process.exit(1);
3506
+ }
3507
+ let archiveBuffer;
3508
+ try {
3509
+ debug("Downloading tarball...");
3510
+ const arrayBuffer = await api2.downloadFile(downloadUrl);
3511
+ archiveBuffer = Buffer.from(arrayBuffer);
3512
+ debug(`Downloaded ${archiveBuffer.length} bytes`);
3513
+ } catch {
3514
+ s.fail("Download failed.");
3515
+ errorWithFix(
3516
+ "Failed to download the package archive.",
3517
+ "Check your network connection and try again."
3518
+ );
3519
+ process.exit(1);
3520
+ }
3521
+ if (!args["skip-verify"] && expectedSha256) {
3522
+ s.text = "Verifying integrity...";
3523
+ debug("Computing SHA-256 digest...");
3524
+ const computedHash = await sha256(archiveBuffer);
3525
+ debug(`Computed SHA-256: ${computedHash}`);
3526
+ if (computedHash !== expectedSha256) {
3527
+ s.fail("Integrity check failed.");
3528
+ errorWithFix(
3529
+ `SHA-256 mismatch!
3530
+ Expected: ${expectedSha256}
3531
+ Got: ${computedHash}`,
3532
+ "The downloaded file may be corrupted. Try running `hubbits pull` again."
3533
+ );
3534
+ if (isJsonMode()) {
3535
+ outputJson({
3536
+ status: "error",
3537
+ error: "integrity_mismatch",
3538
+ expected_sha256: expectedSha256,
3539
+ computed_sha256: computedHash
3540
+ });
3541
+ }
3542
+ process.exit(1);
3543
+ }
3544
+ debug("SHA-256 verified.");
3545
+ } else if (args["skip-verify"]) {
3546
+ debug("Skipping SHA-256 verification (--skip-verify)");
3547
+ } else {
3548
+ debug("No expected SHA-256 available, skipping verification");
3549
+ }
3550
+ const outputDir = args.output ? resolve(args.output) : resolve(process.cwd(), name);
3551
+ if (existsSync(outputDir) && !args.force) {
3552
+ s.fail("Directory already exists.");
3553
+ errorWithFix(
3554
+ `Directory "${name}" already exists.`,
3555
+ "Use `--force` to overwrite or `--output` to specify a different directory."
3556
+ );
3557
+ if (isJsonMode()) {
3558
+ outputJson({
3559
+ status: "error",
3560
+ error: "directory_exists",
3561
+ path: outputDir
3562
+ });
3563
+ }
3564
+ process.exit(1);
3565
+ }
3566
+ s.text = `Extracting to ${pc5.dim(outputDir)}...`;
3567
+ debug(`Extracting to: ${outputDir}`);
3568
+ try {
3569
+ await mkdir(outputDir, { recursive: true });
3570
+ await extract(archiveBuffer, outputDir);
3571
+ } catch (err) {
3572
+ s.fail("Extraction failed.");
3573
+ const errMsg = err instanceof Error ? err.message : String(err);
3574
+ if (errMsg.includes("Symlinks") || errMsg.includes("Path traversal") || errMsg.includes("too large")) {
3575
+ errorWithFix(
3576
+ `Security check failed: ${errMsg}`,
3577
+ "This package archive contains unsafe content and cannot be extracted."
3578
+ );
3579
+ } else {
3580
+ errorWithFix(
3581
+ `Failed to extract package: ${errMsg}`,
3582
+ "The archive may be corrupted. Try running `hubbits pull` again."
3583
+ );
3584
+ }
3585
+ process.exit(1);
3586
+ }
3587
+ s.succeed(`Downloaded ${pc5.cyan(fullName)}@${pc5.yellow(resolvedVersion)}`);
3588
+ if (isJsonMode()) {
3589
+ outputJson({
3590
+ status: "ok",
3591
+ package: fullName,
3592
+ version: resolvedVersion,
3593
+ sha256: expectedSha256 || null,
3594
+ path: outputDir,
3595
+ size: archiveBuffer.length
3596
+ });
3597
+ return;
3598
+ }
3599
+ newline();
3600
+ field("Package", pc5.cyan(fullName));
3601
+ field("Version", pc5.yellow(resolvedVersion));
3602
+ if (expectedSha256) {
3603
+ field("SHA-256", pc5.dim(expectedSha256.slice(0, 16) + "..."));
3604
+ }
3605
+ field("Size", formatBytes(archiveBuffer.length));
3606
+ field("Path", outputDir);
3607
+ newline();
3608
+ hint(`Open \`${name}/\` in your AI editor and type "let's play" to begin`);
3609
+ hint(`View contents: \`ls ${name}/\``);
3610
+ }
3611
+ });
3612
+ function handleApiError(err, packageName) {
3613
+ if (err instanceof ApiError) {
3614
+ switch (err.statusCode) {
3615
+ case 404:
3616
+ errorWithFix(
3617
+ `Package "${packageName}" not found.`,
3618
+ `Run \`hubbits search\` to find available packages.`
3619
+ );
3620
+ break;
3621
+ case 401:
3622
+ errorWithFix(
3623
+ "Authentication required.",
3624
+ "Run `hubbits login` to authenticate first."
3625
+ );
3626
+ break;
3627
+ case 403:
3628
+ errorWithFix(
3629
+ err.message || "Access denied.",
3630
+ "You may need to purchase this package first. Visit https://hubbits.dev to buy it."
3631
+ );
3632
+ break;
3633
+ default:
3634
+ errorWithFix(err.message, "Check your network connection and try again.");
3635
+ }
3636
+ } else {
3637
+ errorWithFix(
3638
+ "An unexpected error occurred.",
3639
+ "Check your network connection and try again."
3640
+ );
3641
+ }
3642
+ }
3643
+ function formatBytes(bytes) {
3644
+ if (bytes < 1024) return `${bytes} B`;
3645
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
3646
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
3647
+ }
3648
+ var SCANNABLE_EXTENSIONS = [".yaml", ".yml", ".md", ".txt"];
3649
+ var MAX_PACKAGE_SIZE = 50 * 1024 * 1024;
3650
+ var publish_default = defineCommand({
3651
+ meta: {
3652
+ name: "publish",
3653
+ description: "Publish the current hubbit to the registry"
3654
+ },
3655
+ args: {
3656
+ path: {
3657
+ type: "positional",
3658
+ description: "Path to the package directory (defaults to current directory)",
3659
+ required: false
3660
+ },
3661
+ "dry-run": {
3662
+ type: "boolean",
3663
+ description: "Validate and pack but do not upload",
3664
+ default: false
3665
+ },
3666
+ "skip-scan": {
3667
+ type: "boolean",
3668
+ description: "Skip security scan (not recommended)",
3669
+ default: false
3670
+ },
3671
+ yes: {
3672
+ type: "boolean",
3673
+ alias: "y",
3674
+ description: "Skip confirmation prompt",
3675
+ default: false
3676
+ },
3677
+ json: {
3678
+ type: "boolean",
3679
+ description: "Output as JSON",
3680
+ default: false
3681
+ },
3682
+ quiet: {
3683
+ type: "boolean",
3684
+ description: "Suppress output",
3685
+ default: false
3686
+ },
3687
+ verbose: {
3688
+ type: "boolean",
3689
+ description: "Show debug information",
3690
+ default: false
3691
+ }
3692
+ },
3693
+ async run({ args }) {
3694
+ setOutputMode({ json: args.json, quiet: args.quiet, verbose: args.verbose });
3695
+ const packageDir = resolve(args.path ?? process.cwd());
3696
+ const auth = getAuthState();
3697
+ if (!auth) {
3698
+ errorWithFix(
3699
+ "You must be logged in to publish packages.",
3700
+ "Run `hubbits login` to authenticate first."
3701
+ );
3702
+ if (isJsonMode()) {
3703
+ outputJson({ status: "error", error: "not_authenticated" });
3704
+ }
3705
+ process.exit(1);
3706
+ }
3707
+ debug(`Authenticated as: ${auth.username ?? auth.email ?? "token"}`);
3708
+ const yamlPath = findManifestFile2(packageDir);
3709
+ if (!yamlPath) {
3710
+ errorWithFix(
3711
+ `No hubbit.yaml found in ${packageDir}`,
3712
+ "Run `hubbits create` to scaffold a new package, or check you are in the correct directory."
3713
+ );
3714
+ if (isJsonMode()) {
3715
+ outputJson({ status: "error", error: "manifest_not_found", path: packageDir });
3716
+ }
3717
+ process.exit(1);
3718
+ }
3719
+ debug(`Reading manifest: ${yamlPath}`);
3720
+ let rawYaml;
3721
+ try {
3722
+ rawYaml = readFileSync(yamlPath, "utf-8");
3723
+ } catch {
3724
+ errorWithFix("Failed to read hubbit.yaml.", "Check file permissions.");
3725
+ process.exit(1);
3726
+ }
3727
+ const parseResult = parseManifest(rawYaml);
3728
+ if (!parseResult.success) {
3729
+ if (isJsonMode()) {
3730
+ outputJson({ status: "error", error: "invalid_manifest", errors: parseResult.errors });
3731
+ process.exit(1);
3732
+ }
3733
+ error("Invalid hubbit.yaml:");
3734
+ newline();
3735
+ for (const err of parseResult.errors) {
3736
+ console.log(` ${pc5.red("\u2717")} ${err}`);
3737
+ }
3738
+ newline();
3739
+ hint("Run `hubbits validate` for detailed validation.");
3740
+ process.exit(1);
3741
+ }
3742
+ const manifest = {
3743
+ ...parseResult.data,
3744
+ ...!parseResult.data.author && auth.username ? { author: auth.username } : {}
3745
+ };
3746
+ const validationResult = validateManifest2(manifest);
3747
+ if (!validationResult.success) {
3748
+ if (isJsonMode()) {
3749
+ outputJson({ status: "error", error: "validation_failed", errors: validationResult.errors });
3750
+ process.exit(1);
3751
+ }
3752
+ error("Manifest validation failed for publishing:");
3753
+ newline();
3754
+ for (const err of validationResult.errors) {
3755
+ console.log(` ${pc5.red("\u2717")} ${err}`);
3756
+ }
3757
+ newline();
3758
+ hint("Run `hubbits validate --strict` to check all publishing requirements.");
3759
+ process.exit(1);
3760
+ }
3761
+ const packageName = manifest.name;
3762
+ const packageVersion = manifest.version;
3763
+ debug(`Package: ${packageName}@${packageVersion}`);
3764
+ if (!args["skip-scan"]) {
3765
+ const scanSpinner = spinner("Scanning for security threats...");
3766
+ const scanIssues = await scanDirectory(packageDir);
3767
+ if (scanIssues.length > 0) {
3768
+ const criticalOrHigh = scanIssues.filter(
3769
+ (i) => i.startsWith("[CRITICAL]") || i.startsWith("[HIGH]")
3770
+ );
3771
+ if (criticalOrHigh.length > 0) {
3772
+ scanSpinner.fail("Security scan found threats.");
3773
+ newline();
3774
+ error(`Found ${criticalOrHigh.length} security issue${criticalOrHigh.length === 1 ? "" : "s"}:`);
3775
+ newline();
3776
+ for (const issue of scanIssues) {
3777
+ const color = issue.startsWith("[CRITICAL]") ? pc5.red : issue.startsWith("[HIGH]") ? pc5.yellow : pc5.dim;
3778
+ console.log(` ${color(issue)}`);
3779
+ }
3780
+ newline();
3781
+ if (isJsonMode()) {
3782
+ outputJson({ status: "error", error: "security_scan_failed", issues: scanIssues });
3783
+ process.exit(1);
3784
+ }
3785
+ errorWithFix(
3786
+ "Package contains potential security threats and cannot be published.",
3787
+ "Review and fix the issues above. Use `--skip-scan` to bypass (not recommended)."
3788
+ );
3789
+ process.exit(1);
3790
+ }
3791
+ scanSpinner.warn(`Security scan found ${scanIssues.length} warning${scanIssues.length === 1 ? "" : "s"}.`);
3792
+ for (const issue of scanIssues) {
3793
+ console.log(` ${pc5.dim(issue)}`);
3794
+ }
3795
+ } else {
3796
+ scanSpinner.succeed("Security scan passed.");
3797
+ }
3798
+ } else {
3799
+ debug("Skipping security scan (--skip-scan)");
3800
+ }
3801
+ const packSpinner = spinner("Packing archive...");
3802
+ let archiveBuffer;
3803
+ try {
3804
+ debug(`Packing directory: ${packageDir}`);
3805
+ archiveBuffer = await pack(packageDir);
3806
+ debug(`Archive size: ${archiveBuffer.length} bytes`);
3807
+ if (archiveBuffer.length > MAX_PACKAGE_SIZE) {
3808
+ packSpinner.fail("Package too large.");
3809
+ errorWithFix(
3810
+ `Package size (${formatBytes2(archiveBuffer.length)}) exceeds the maximum of ${formatBytes2(MAX_PACKAGE_SIZE)}.`,
3811
+ "Remove unnecessary files or large assets to reduce the package size."
3812
+ );
3813
+ process.exit(1);
3814
+ }
3815
+ packSpinner.succeed(`Packed ${formatBytes2(archiveBuffer.length)}`);
3816
+ } catch (err) {
3817
+ packSpinner.fail("Packing failed.");
3818
+ const errMsg = err instanceof Error ? err.message : String(err);
3819
+ errorWithFix(
3820
+ `Failed to pack archive: ${errMsg}`,
3821
+ "Check file permissions and ensure no symlinks exist in the package directory."
3822
+ );
3823
+ process.exit(1);
3824
+ }
3825
+ const digestSpinner = spinner("Computing digest...");
3826
+ const digest = await sha256(archiveBuffer);
3827
+ digestSpinner.succeed(`Digest: ${pc5.dim(digest.slice(0, 16) + "...")}`);
3828
+ debug(`SHA-256: ${digest}`);
3829
+ if (args["dry-run"]) {
3830
+ newline();
3831
+ success(`Dry run complete for ${pc5.bold(packageName)}@${pc5.yellow(packageVersion)}`);
3832
+ newline();
3833
+ field("Package", packageName);
3834
+ field("Version", packageVersion);
3835
+ field("Size", formatBytes2(archiveBuffer.length));
3836
+ field("SHA-256", digest);
3837
+ newline();
3838
+ hint("Remove `--dry-run` to publish for real.");
3839
+ if (isJsonMode()) {
3840
+ outputJson({
3841
+ status: "dry_run",
3842
+ package: packageName,
3843
+ version: packageVersion,
3844
+ size: archiveBuffer.length,
3845
+ sha256: digest
3846
+ });
3847
+ }
3848
+ return;
3849
+ }
3850
+ if (!args.yes && !args.json && !args.quiet) {
3851
+ newline();
3852
+ info(`About to publish:`);
3853
+ field("Package", pc5.cyan(packageName));
3854
+ field("Version", pc5.yellow(packageVersion));
3855
+ field("Size", formatBytes2(archiveBuffer.length));
3856
+ if (auth.username) {
3857
+ field("Author", auth.username);
3858
+ }
3859
+ newline();
3860
+ const confirmed = await p5.confirm({
3861
+ message: `Publish ${pc5.bold(packageName)}@${pc5.yellow(packageVersion)}?`
3862
+ });
3863
+ if (p5.isCancel(confirmed) || !confirmed) {
3864
+ p5.cancel("Publish cancelled.");
3865
+ process.exit(0);
3866
+ }
3867
+ }
3868
+ const api2 = createApiClient();
3869
+ const publishSpinner = spinner("Requesting upload URL...");
3870
+ let uploadUrl;
3871
+ let resolvedScope;
3872
+ let resolvedName;
3873
+ for (let attempt = 0; ; attempt++) {
3874
+ try {
3875
+ debug("PUT /api/v1/packages/publish");
3876
+ const result = await api2.requestPublish(
3877
+ manifest,
3878
+ digest,
3879
+ archiveBuffer.length
3880
+ );
3881
+ if (!result.data) {
3882
+ publishSpinner.fail("Failed to initiate publish.");
3883
+ error("Server returned an unexpected response.");
3884
+ process.exit(1);
3885
+ }
3886
+ uploadUrl = result.data.upload_url;
3887
+ resolvedScope = result.data.scope;
3888
+ resolvedName = result.data.name;
3889
+ debug(`Upload URL received for ${resolvedScope}/${resolvedName}@${result.data.version}`);
3890
+ break;
3891
+ } catch (err) {
3892
+ if (err instanceof ApiError && err.statusCode === 429 && attempt < 2) {
3893
+ const waitSec = parseRetryAfter(err.message) ?? 60;
3894
+ for (let t = waitSec; t > 0; t--) {
3895
+ publishSpinner.text = `Rate limited \u2014 retrying in ${t}s...`;
3896
+ await sleep2(1e3);
3897
+ }
3898
+ publishSpinner.text = "Requesting upload URL...";
3899
+ continue;
3900
+ }
3901
+ publishSpinner.fail("Publish request failed.");
3902
+ handlePublishError(err, packageName, packageVersion);
3903
+ process.exit(1);
3904
+ }
3905
+ }
3906
+ publishSpinner.text = "Uploading package...";
3907
+ try {
3908
+ debug(`Uploading ${archiveBuffer.length} bytes...`);
3909
+ await api2.uploadFile(uploadUrl, archiveBuffer);
3910
+ debug("Upload complete.");
3911
+ } catch {
3912
+ publishSpinner.fail("Upload failed.");
3913
+ errorWithFix(
3914
+ "Failed to upload the package archive.",
3915
+ "Check your network connection and try again."
3916
+ );
3917
+ process.exit(1);
3918
+ }
3919
+ publishSpinner.text = "Confirming publish...";
3920
+ let confirmResult;
3921
+ for (let attempt = 0; ; attempt++) {
3922
+ try {
3923
+ debug("POST /api/v1/packages/publish/confirm");
3924
+ confirmResult = await api2.confirmPublish(
3925
+ resolvedScope,
3926
+ resolvedName,
3927
+ packageVersion,
3928
+ digest,
3929
+ manifest
3930
+ );
3931
+ break;
3932
+ } catch (err) {
3933
+ if (err instanceof ApiError && err.statusCode === 429 && attempt < 2) {
3934
+ const waitSec = parseRetryAfter(err.message) ?? 60;
3935
+ for (let t = waitSec; t > 0; t--) {
3936
+ publishSpinner.text = `Rate limited \u2014 retrying confirmation in ${t}s...`;
3937
+ await sleep2(1e3);
3938
+ }
3939
+ publishSpinner.text = "Confirming publish...";
3940
+ continue;
3941
+ }
3942
+ publishSpinner.fail("Publish confirmation failed.");
3943
+ handlePublishError(err, packageName, packageVersion);
3944
+ process.exit(1);
3945
+ }
3946
+ }
3947
+ if (!confirmResult?.data) {
3948
+ publishSpinner.fail("Publish confirmation failed.");
3949
+ error("Server returned an unexpected response.");
3950
+ process.exit(1);
3951
+ }
3952
+ publishSpinner.succeed("Published!");
3953
+ const confirmedData = confirmResult.data;
3954
+ const fullPublishedName = confirmedData.name;
3955
+ const publishUrl = confirmedData.url;
3956
+ if (isJsonMode()) {
3957
+ outputJson({
3958
+ status: "ok",
3959
+ package: fullPublishedName,
3960
+ version: packageVersion,
3961
+ sha256: digest,
3962
+ size: archiveBuffer.length,
3963
+ signature: confirmedData.signature,
3964
+ url: publishUrl
3965
+ });
3966
+ return;
3967
+ }
3968
+ newline();
3969
+ success(`Published ${pc5.bold(pc5.cyan(fullPublishedName))}@${pc5.yellow(packageVersion)}`);
3970
+ newline();
3971
+ field("URL", publishUrl);
3972
+ field("SHA-256", pc5.dim(digest.slice(0, 16) + "..."));
3973
+ field("Size", formatBytes2(archiveBuffer.length));
3974
+ if (confirmedData.signature) {
3975
+ field("Signature", pc5.dim(confirmedData.signature.slice(0, 16) + "..."));
3976
+ }
3977
+ newline();
3978
+ hint(`View: ${publishUrl}`);
3979
+ hint(`Install: \`hubbits pull ${fullPublishedName}\``);
3980
+ }
3981
+ });
3982
+ async function scanDirectory(dir) {
3983
+ const allIssues = [];
3984
+ try {
3985
+ const files = collectScannableFiles(dir, dir);
3986
+ debug(`Scanning ${files.length} file(s) for security threats...`);
3987
+ for (const filePath of files) {
3988
+ try {
3989
+ const content = readFileSync(filePath, "utf-8");
3990
+ const result = scanPromptInjection(content);
3991
+ if (result.issues.length > 0) {
3992
+ const relPath = filePath.slice(dir.length + 1);
3993
+ for (const issue of result.issues) {
3994
+ allIssues.push(`${relPath}: ${issue}`);
3995
+ }
3996
+ }
3997
+ } catch {
3998
+ debug(`Skipping unreadable file: ${filePath}`);
3999
+ }
4000
+ }
4001
+ } catch (err) {
4002
+ debug(`Security scan error: ${err instanceof Error ? err.message : String(err)}`);
4003
+ }
4004
+ return allIssues;
4005
+ }
4006
+ function collectScannableFiles(dir, rootDir) {
4007
+ const files = [];
4008
+ try {
4009
+ const entries = readdirSync(dir, { withFileTypes: true });
4010
+ for (const entry of entries) {
4011
+ const fullPath = join(dir, entry.name);
4012
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
4013
+ continue;
4014
+ }
4015
+ if (entry.isDirectory() && !entry.isSymbolicLink()) {
4016
+ files.push(...collectScannableFiles(fullPath, rootDir));
4017
+ } else if (entry.isFile()) {
4018
+ const ext = entry.name.slice(entry.name.lastIndexOf("."));
4019
+ if (SCANNABLE_EXTENSIONS.includes(ext)) {
4020
+ files.push(fullPath);
4021
+ }
4022
+ }
4023
+ }
4024
+ } catch {
4025
+ }
4026
+ return files;
4027
+ }
4028
+ function findManifestFile2(dir) {
4029
+ const candidates = ["hubbit.yaml", "hubbit.yml"];
4030
+ for (const name of candidates) {
4031
+ const filePath = join(dir, name);
4032
+ if (existsSync(filePath)) return filePath;
4033
+ }
4034
+ return null;
4035
+ }
4036
+ function handlePublishError(err, packageName, version) {
4037
+ if (err instanceof ApiError) {
4038
+ switch (err.statusCode) {
4039
+ case 401:
4040
+ errorWithFix(
4041
+ "Authentication expired.",
4042
+ "Run `hubbits login` to re-authenticate."
4043
+ );
4044
+ break;
4045
+ case 403:
4046
+ errorWithFix(
4047
+ err.message || "You do not have permission to publish this package.",
4048
+ "Check that you are the package owner, or create a new package with a different name."
4049
+ );
4050
+ break;
4051
+ case 409:
4052
+ errorWithFix(
4053
+ `Version "${version}" already exists for "${packageName}".`,
4054
+ `Bump the version in hubbit.yaml and try again.`
4055
+ );
4056
+ break;
4057
+ case 422:
4058
+ error(`Validation error: ${err.message}`);
4059
+ if (err.details) {
4060
+ for (const [fieldName, msgs] of Object.entries(err.details)) {
4061
+ for (const msg of msgs) {
4062
+ console.log(` ${pc5.red("\u2717")} ${fieldName}: ${msg}`);
4063
+ }
4064
+ }
4065
+ }
4066
+ hint("Fix the validation errors above and try again.");
4067
+ break;
4068
+ default:
4069
+ errorWithFix(err.message, "Check your network connection and try again.");
4070
+ }
4071
+ } else {
4072
+ errorWithFix(
4073
+ "An unexpected error occurred.",
4074
+ "Check your network connection and try again."
4075
+ );
4076
+ }
4077
+ }
4078
+ function formatBytes2(bytes) {
4079
+ if (bytes < 1024) return `${bytes} B`;
4080
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
4081
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
4082
+ }
4083
+ function parseRetryAfter(message) {
4084
+ const match = message.match(/retry after (\d+)/i);
4085
+ return match ? parseInt(match[1], 10) : null;
4086
+ }
4087
+ function sleep2(ms) {
4088
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
4089
+ }
4090
+ var COMMANDS = [
4091
+ "login",
4092
+ "logout",
4093
+ "search",
4094
+ "init",
4095
+ "create",
4096
+ "validate",
4097
+ "pull",
4098
+ "publish",
4099
+ "completion"
4100
+ ];
4101
+ var SHELLS = ["zsh", "bash", "fish", "powershell"];
4102
+ var completion_default = defineCommand({
4103
+ meta: {
4104
+ name: "completion",
4105
+ description: "Generate shell tab-completion script"
4106
+ },
4107
+ args: {
4108
+ shell: {
4109
+ type: "positional",
4110
+ description: "Shell to generate completions for (zsh | bash | fish | powershell)",
4111
+ required: false
4112
+ }
4113
+ },
4114
+ run({ args }) {
4115
+ const shell = args.shell ?? detectShell();
4116
+ if (!shell || !SHELLS.includes(shell)) {
4117
+ errorWithFix(
4118
+ `Unknown shell: "${shell ?? ""}"`,
4119
+ `Run \`hubbits completion <shell>\` where shell is one of: ${SHELLS.join(", ")}`
4120
+ );
4121
+ process.exit(1);
4122
+ }
4123
+ const script = generateScript(shell);
4124
+ process.stdout.write(script + "\n");
4125
+ }
4126
+ });
4127
+ function detectShell() {
4128
+ const shell = process.env.SHELL ?? "";
4129
+ if (shell.endsWith("zsh")) return "zsh";
4130
+ if (shell.endsWith("bash")) return "bash";
4131
+ if (shell.endsWith("fish")) return "fish";
4132
+ if (process.platform === "win32") return "powershell";
4133
+ return null;
4134
+ }
4135
+ function generateScript(shell) {
4136
+ switch (shell) {
4137
+ case "zsh":
4138
+ return zshScript();
4139
+ case "bash":
4140
+ return bashScript();
4141
+ case "fish":
4142
+ return fishScript();
4143
+ case "powershell":
4144
+ return powershellScript();
4145
+ }
4146
+ }
4147
+ function zshScript() {
4148
+ return `# Hubbits CLI \u2014 zsh completion
4149
+ # Add to ~/.zshrc: source <(hubbits completion zsh)
4150
+
4151
+ _hubbits_completions() {
4152
+ local -a commands
4153
+ commands=(
4154
+ ${COMMANDS.map((c) => `'${c}:${cmdDescription(c)}'`).join("\n ")}
4155
+ )
4156
+
4157
+ if (( CURRENT == 2 )); then
4158
+ _describe 'hubbits command' commands
4159
+ elif (( CURRENT >= 3 )); then
4160
+ case \${words[2]} in
4161
+ completion)
4162
+ local -a shells
4163
+ shells=('zsh:zsh completion' 'bash:bash completion' 'fish:fish completion' 'powershell:PowerShell completion')
4164
+ _describe 'shell' shells
4165
+ ;;
4166
+ pull|validate)
4167
+ _files
4168
+ ;;
4169
+ esac
4170
+ fi
4171
+ }
4172
+
4173
+ if [[ -n "\${ZSH_VERSION:-}" ]]; then
4174
+ if (( \${+functions[compdef]} )); then
4175
+ compdef _hubbits_completions hubbits
4176
+ else
4177
+ autoload -Uz compinit && compinit
4178
+ compdef _hubbits_completions hubbits
4179
+ fi
4180
+ fi`;
4181
+ }
4182
+ function bashScript() {
4183
+ const cmdsQuoted = COMMANDS.map((c) => `"${c}"`).join(" ");
4184
+ return `# Hubbits CLI \u2014 bash completion
4185
+ # Add to ~/.bashrc: source <(hubbits completion bash)
4186
+
4187
+ _hubbits_completions() {
4188
+ local cur prev words
4189
+ _init_completion 2>/dev/null || {
4190
+ COMPREPLY=()
4191
+ cur=\${COMP_WORDS[COMP_CWORD]}
4192
+ prev=\${COMP_WORDS[COMP_CWORD-1]}
4193
+ }
4194
+
4195
+ local commands=(${cmdsQuoted})
4196
+
4197
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
4198
+ COMPREPLY=( $(compgen -W "\${commands[*]}" -- "$cur") )
4199
+ return
4200
+ fi
4201
+
4202
+ case "$prev" in
4203
+ completion)
4204
+ COMPREPLY=( $(compgen -W "zsh bash fish powershell" -- "$cur") )
4205
+ ;;
4206
+ pull|validate)
4207
+ COMPREPLY=( $(compgen -f -- "$cur") )
4208
+ ;;
4209
+ esac
4210
+ }
4211
+
4212
+ complete -F _hubbits_completions hubbits`;
4213
+ }
4214
+ function fishScript() {
4215
+ const cmdCompletions = COMMANDS.map(
4216
+ (c) => `complete -c hubbits -f -n '__fish_use_subcommand' -a '${c}' -d '${cmdDescription(c)}'`
4217
+ ).join("\n");
4218
+ return `# Hubbits CLI \u2014 fish completion
4219
+ # Save to ~/.config/fish/completions/hubbits.fish
4220
+
4221
+ # Disable file completions for hubbits
4222
+ complete -c hubbits -f
4223
+
4224
+ # Top-level subcommands
4225
+ ${cmdCompletions}
4226
+
4227
+ # completion subcommand \u2014 shell argument
4228
+ complete -c hubbits -n '__fish_seen_subcommand_from completion' -a 'zsh' -d 'zsh completion script'
4229
+ complete -c hubbits -n '__fish_seen_subcommand_from completion' -a 'bash' -d 'bash completion script'
4230
+ complete -c hubbits -n '__fish_seen_subcommand_from completion' -a 'fish' -d 'fish completion script'
4231
+ complete -c hubbits -n '__fish_seen_subcommand_from completion' -a 'powershell' -d 'PowerShell completion script'
4232
+
4233
+ # pull / validate \u2014 allow file paths
4234
+ complete -c hubbits -n '__fish_seen_subcommand_from pull validate' -a '(__fish_complete_path)'`;
4235
+ }
4236
+ function powershellScript() {
4237
+ const cmdsArray = COMMANDS.map((c) => `'${c}'`).join(", ");
4238
+ return `# Hubbits CLI \u2014 PowerShell completion
4239
+ # Add to your PowerShell profile ($PROFILE):
4240
+ # Invoke-Expression (hubbits completion powershell)
4241
+
4242
+ Register-ArgumentCompleter -Native -CommandName @('hubbits') -ScriptBlock {
4243
+ param($wordToComplete, $commandAst, $cursorPosition)
4244
+
4245
+ $commands = @(${cmdsArray})
4246
+ $shells = @('zsh', 'bash', 'fish', 'powershell')
4247
+
4248
+ $tokens = $commandAst.CommandElements
4249
+ $subCmd = if ($tokens.Count -ge 2) { $tokens[1].ToString() } else { $null }
4250
+
4251
+ if ($tokens.Count -le 2) {
4252
+ # Complete top-level commands
4253
+ $commands | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
4254
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
4255
+ }
4256
+ } elseif ($subCmd -eq 'completion') {
4257
+ $shells | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
4258
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', "$_ completion script")
4259
+ }
4260
+ }
4261
+ }`;
4262
+ }
4263
+ function cmdDescription(cmd) {
4264
+ const descriptions = {
4265
+ login: "Log in to your Hubbits account",
4266
+ logout: "Log out of your Hubbits account",
4267
+ search: "Search for hubbits",
4268
+ init: "Initialize a new hubbit in the current directory",
4269
+ create: "Create a new hubbit or import from GitHub",
4270
+ validate: "Validate a hubbit.yaml manifest",
4271
+ pull: "Download a hubbit package",
4272
+ publish: "Publish a hubbit package",
4273
+ completion: "Generate shell tab-completion script"
4274
+ };
4275
+ return descriptions[cmd] ?? cmd;
4276
+ }
4277
+
4278
+ // src/index.ts
4279
+ if (process.env.HUBBITS_CWD) {
4280
+ process.chdir(process.env.HUBBITS_CWD);
4281
+ }
4282
+ var main = defineCommand({
4283
+ meta: {
4284
+ name: "hubbits",
4285
+ version: "0.1.0",
4286
+ description: "Hubbits CLI - Interactive AI experience manager"
4287
+ },
4288
+ args: {
4289
+ version: {
4290
+ type: "boolean",
4291
+ alias: "v",
4292
+ description: "Show version"
4293
+ }
4294
+ },
4295
+ subCommands: {
4296
+ login: login_default,
4297
+ logout: logout_default,
4298
+ search: search_default,
4299
+ init: init_default,
4300
+ create: create_default,
4301
+ validate: validate_default,
4302
+ pull: pull_default,
4303
+ publish: publish_default,
4304
+ completion: completion_default
4305
+ },
4306
+ run({ args, rawArgs }) {
4307
+ if (args.version) {
4308
+ console.log("hubbits/0.1.0");
4309
+ return;
4310
+ }
4311
+ if (rawArgs.some((a) => !a.startsWith("-"))) return;
4312
+ console.log("Hubbits CLI v0.1.0");
4313
+ console.log("Run `hubbits --help` for available commands.");
4314
+ }
4315
+ });
4316
+ runMain(main);