@hogsend/cli 0.1.0 → 0.2.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/bin.js +575 -104
- package/dist/bin.js.map +1 -1
- package/package.json +4 -1
- package/skills/hogsend-authoring-buckets/SKILL.md +69 -0
- package/skills/hogsend-authoring-buckets/references/bucket-id-aliases.md +117 -0
- package/skills/hogsend-authoring-buckets/references/bucket-meta.md +142 -0
- package/skills/hogsend-authoring-buckets/references/buckets-vs-journeys.md +96 -0
- package/skills/hogsend-authoring-buckets/references/register-a-bucket.md +129 -0
- package/skills/hogsend-authoring-emails/SKILL.md +68 -0
- package/skills/hogsend-authoring-emails/references/email-components.md +112 -0
- package/skills/hogsend-authoring-emails/references/preview-and-render.md +116 -0
- package/skills/hogsend-authoring-emails/references/template-four-file-contract.md +134 -0
- package/skills/hogsend-authoring-emails/references/tracking-and-unsubscribe.md +127 -0
- package/skills/hogsend-authoring-journeys/SKILL.md +88 -0
- package/skills/hogsend-authoring-journeys/references/branch-on-engagement.md +93 -0
- package/skills/hogsend-authoring-journeys/references/journey-context.md +110 -0
- package/skills/hogsend-authoring-journeys/references/journey-meta.md +142 -0
- package/skills/hogsend-authoring-journeys/references/register-a-journey.md +99 -0
- package/skills/hogsend-authoring-journeys/references/sending-email-from-a-journey.md +82 -0
- package/skills/hogsend-cli/SKILL.md +1 -0
- package/skills/hogsend-conditions/SKILL.md +70 -0
- package/skills/hogsend-conditions/references/condition-types.md +251 -0
- package/skills/hogsend-conditions/references/durations.md +90 -0
- package/skills/hogsend-conditions/references/examples.md +188 -0
- package/skills/hogsend-database/SKILL.md +70 -0
- package/skills/hogsend-database/references/client-track-schema.md +97 -0
- package/skills/hogsend-database/references/migrations.md +132 -0
- package/skills/hogsend-database/references/schema-drift.md +123 -0
- package/skills/hogsend-deploy/SKILL.md +62 -0
- package/skills/hogsend-deploy/references/env-and-secrets.md +118 -0
- package/skills/hogsend-deploy/references/railway-two-services.md +122 -0
- package/skills/hogsend-deploy/references/upgrade-engine.md +92 -0
- package/skills/hogsend-webhooks-and-workflows/SKILL.md +68 -0
- package/skills/hogsend-webhooks-and-workflows/references/backfill-pattern.md +148 -0
- package/skills/hogsend-webhooks-and-workflows/references/custom-workflow.md +156 -0
- package/skills/hogsend-webhooks-and-workflows/references/webhook-source.md +172 -0
- package/src/commands/doctor.ts +22 -0
- package/src/commands/index.ts +4 -0
- package/src/commands/skills.ts +36 -96
- package/src/commands/studio.ts +261 -0
- package/src/commands/upgrade.ts +245 -0
- package/src/lib/skills.ts +186 -0
- package/studio/assets/index-BVA9GZqq.css +1 -0
- package/studio/assets/index-kPwzOOyG.js +230 -0
- package/studio/index.html +13 -0
package/dist/bin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/bin.ts
|
|
4
|
-
import { createRequire as
|
|
4
|
+
import { createRequire as createRequire3 } from "module";
|
|
5
5
|
|
|
6
6
|
// src/commands/contacts.ts
|
|
7
7
|
import { parseArgs } from "util";
|
|
@@ -110,10 +110,10 @@ function renderTable(rows, columns) {
|
|
|
110
110
|
);
|
|
111
111
|
const pad = (text, width) => text + " ".repeat(width - text.length);
|
|
112
112
|
const header = cols.map((c, i) => color.bold(pad(c, widths[i] ?? 0))).join(" ");
|
|
113
|
-
const
|
|
113
|
+
const sep4 = cols.map((_, i) => "-".repeat(widths[i] ?? 0)).join(" ");
|
|
114
114
|
const body = rows.map((r) => cols.map((c, i) => pad(cell(r[c]), widths[i] ?? 0)).join(" ")).join("\n");
|
|
115
115
|
return `${header}
|
|
116
|
-
${color.dim(
|
|
116
|
+
${color.dim(sep4)}
|
|
117
117
|
${body}`;
|
|
118
118
|
}
|
|
119
119
|
function renderKv(obj) {
|
|
@@ -430,6 +430,137 @@ var contactsCommand = {
|
|
|
430
430
|
|
|
431
431
|
// src/commands/doctor.ts
|
|
432
432
|
import { parseArgs as parseArgs2 } from "util";
|
|
433
|
+
|
|
434
|
+
// src/lib/skills.ts
|
|
435
|
+
import {
|
|
436
|
+
cpSync,
|
|
437
|
+
existsSync,
|
|
438
|
+
mkdirSync,
|
|
439
|
+
readdirSync,
|
|
440
|
+
readFileSync,
|
|
441
|
+
statSync,
|
|
442
|
+
writeFileSync
|
|
443
|
+
} from "fs";
|
|
444
|
+
import { createRequire } from "module";
|
|
445
|
+
import { join } from "path";
|
|
446
|
+
import { fileURLToPath } from "url";
|
|
447
|
+
function bundledSkillsDir() {
|
|
448
|
+
return fileURLToPath(new URL("../skills", import.meta.url));
|
|
449
|
+
}
|
|
450
|
+
function installDir(cwd) {
|
|
451
|
+
return join(cwd, ".claude", "skills");
|
|
452
|
+
}
|
|
453
|
+
function stampPath(cwd) {
|
|
454
|
+
return join(cwd, ".claude", ".hogsend-skills.json");
|
|
455
|
+
}
|
|
456
|
+
function cliVersion() {
|
|
457
|
+
try {
|
|
458
|
+
const require2 = createRequire(import.meta.url);
|
|
459
|
+
const pkg = require2("../package.json");
|
|
460
|
+
return pkg.version ?? "0.0.0";
|
|
461
|
+
} catch {
|
|
462
|
+
return "0.0.0";
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function readFileSyncSafe(path) {
|
|
466
|
+
try {
|
|
467
|
+
return readFileSync(path, "utf8");
|
|
468
|
+
} catch {
|
|
469
|
+
return "";
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function readFrontmatterField(skillDir, field) {
|
|
473
|
+
const skillFile = join(skillDir, "SKILL.md");
|
|
474
|
+
if (!existsSync(skillFile)) return "";
|
|
475
|
+
const raw = readFileSyncSafe(skillFile);
|
|
476
|
+
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
477
|
+
if (!fmMatch) return "";
|
|
478
|
+
const block = fmMatch[1] ?? "";
|
|
479
|
+
for (const line of block.split("\n")) {
|
|
480
|
+
const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
481
|
+
if (m && m[1] === field) {
|
|
482
|
+
return (m[2] ?? "").replace(/^["']|["']$/g, "").trim();
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return "";
|
|
486
|
+
}
|
|
487
|
+
function listBundledSkills(cwd) {
|
|
488
|
+
const dir = bundledSkillsDir();
|
|
489
|
+
if (!existsSync(dir)) return [];
|
|
490
|
+
const target = installDir(cwd);
|
|
491
|
+
const entries = readdirSync(dir).filter((name) => {
|
|
492
|
+
const full = join(dir, name);
|
|
493
|
+
return statSync(full).isDirectory() && existsSync(join(full, "SKILL.md"));
|
|
494
|
+
});
|
|
495
|
+
return entries.sort().map((name) => ({
|
|
496
|
+
name,
|
|
497
|
+
description: readFrontmatterField(join(dir, name), "description"),
|
|
498
|
+
installed: existsSync(join(target, name))
|
|
499
|
+
}));
|
|
500
|
+
}
|
|
501
|
+
function copySkill(name, cwd, force) {
|
|
502
|
+
const src = join(bundledSkillsDir(), name);
|
|
503
|
+
const dest = join(installDir(cwd), name);
|
|
504
|
+
const exists = existsSync(dest);
|
|
505
|
+
if (exists && !force) {
|
|
506
|
+
return { name, installed: false, skipped: true, path: dest };
|
|
507
|
+
}
|
|
508
|
+
mkdirSync(installDir(cwd), { recursive: true });
|
|
509
|
+
cpSync(src, dest, { recursive: true, force: true });
|
|
510
|
+
return { name, installed: true, skipped: false, path: dest };
|
|
511
|
+
}
|
|
512
|
+
function writeSkillsStamp(cwd, skills) {
|
|
513
|
+
const stamp = {
|
|
514
|
+
cliVersion: cliVersion(),
|
|
515
|
+
skills: [...skills].sort(),
|
|
516
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
517
|
+
};
|
|
518
|
+
mkdirSync(join(cwd, ".claude"), { recursive: true });
|
|
519
|
+
writeFileSync(stampPath(cwd), `${JSON.stringify(stamp, null, 2)}
|
|
520
|
+
`);
|
|
521
|
+
}
|
|
522
|
+
function readSkillsStamp(cwd) {
|
|
523
|
+
try {
|
|
524
|
+
const parsed = JSON.parse(readFileSync(stampPath(cwd), "utf8"));
|
|
525
|
+
return parsed && typeof parsed.cliVersion === "string" ? parsed : null;
|
|
526
|
+
} catch {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function compareVersions(a, b) {
|
|
531
|
+
const parse = (v) => (v.split("-")[0] ?? "").split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
532
|
+
const pa = parse(a);
|
|
533
|
+
const pb = parse(b);
|
|
534
|
+
for (let i = 0; i < 3; i++) {
|
|
535
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
536
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
537
|
+
}
|
|
538
|
+
return 0;
|
|
539
|
+
}
|
|
540
|
+
function skillsStaleness(cwd) {
|
|
541
|
+
const stamp = readSkillsStamp(cwd);
|
|
542
|
+
if (!stamp) return null;
|
|
543
|
+
const current = cliVersion();
|
|
544
|
+
return {
|
|
545
|
+
stale: compareVersions(stamp.cliVersion, current) < 0,
|
|
546
|
+
installed: stamp.cliVersion,
|
|
547
|
+
current
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/commands/doctor.ts
|
|
552
|
+
function skillsNudge(ctx) {
|
|
553
|
+
const verdict = skillsStaleness(process.cwd());
|
|
554
|
+
if (!verdict?.stale || ctx.json) return;
|
|
555
|
+
ctx.out.note(
|
|
556
|
+
[
|
|
557
|
+
`Vendored Claude skills are from v${verdict.installed}; this CLI is v${verdict.current}.`,
|
|
558
|
+
"",
|
|
559
|
+
`Refresh: ${color.cyan("hogsend upgrade")} ${color.dim("(deps + skills)")} or ${color.cyan("hogsend skills add --all --force")}.`
|
|
560
|
+
].join("\n"),
|
|
561
|
+
"Skills out of date"
|
|
562
|
+
);
|
|
563
|
+
}
|
|
433
564
|
var usage2 = `hogsend doctor [--url <baseUrl>] [--admin-key <key>] [--json]
|
|
434
565
|
|
|
435
566
|
Probe a running Hogsend instance via GET /v1/health and report its health:
|
|
@@ -538,7 +669,8 @@ async function run2(ctx) {
|
|
|
538
669
|
uptime: health.uptime,
|
|
539
670
|
timestamp: health.timestamp,
|
|
540
671
|
components: health.components,
|
|
541
|
-
schema: health.schema
|
|
672
|
+
schema: health.schema,
|
|
673
|
+
skills: skillsStaleness(process.cwd()) ?? void 0
|
|
542
674
|
});
|
|
543
675
|
if (!ok) process.exit(1);
|
|
544
676
|
return;
|
|
@@ -561,6 +693,7 @@ async function run2(ctx) {
|
|
|
561
693
|
` ${trackLine("client", health.schema.client)}`
|
|
562
694
|
];
|
|
563
695
|
ctx.out.note(lines.join("\n"), "Doctor");
|
|
696
|
+
skillsNudge(ctx);
|
|
564
697
|
if (ok) {
|
|
565
698
|
ctx.out.outro(color.green("doctor: ok"));
|
|
566
699
|
return;
|
|
@@ -576,15 +709,15 @@ var doctorCommand = {
|
|
|
576
709
|
};
|
|
577
710
|
|
|
578
711
|
// src/commands/eject.ts
|
|
579
|
-
import { existsSync as
|
|
580
|
-
import { createRequire } from "module";
|
|
581
|
-
import { dirname, join as
|
|
712
|
+
import { existsSync as existsSync3, realpathSync } from "fs";
|
|
713
|
+
import { createRequire as createRequire2 } from "module";
|
|
714
|
+
import { dirname, join as join3, sep as sep2 } from "path";
|
|
582
715
|
import { parseArgs as parseArgs3 } from "util";
|
|
583
716
|
|
|
584
717
|
// src/eject.ts
|
|
585
|
-
import { existsSync } from "fs";
|
|
718
|
+
import { existsSync as existsSync2 } from "fs";
|
|
586
719
|
import { cp, readFile, rm, stat, writeFile } from "fs/promises";
|
|
587
|
-
import { basename, join, relative, sep } from "path";
|
|
720
|
+
import { basename, join as join2, relative, sep } from "path";
|
|
588
721
|
var EjectError = class extends Error {
|
|
589
722
|
constructor(message) {
|
|
590
723
|
super(message);
|
|
@@ -609,8 +742,8 @@ async function writePackageJson(file, value) {
|
|
|
609
742
|
async function eject(opts) {
|
|
610
743
|
const { pkg, consumerRoot, sourceDir, force = false } = opts;
|
|
611
744
|
const vendorName = basename(pkg);
|
|
612
|
-
const vendorPath =
|
|
613
|
-
const consumerPkgPath =
|
|
745
|
+
const vendorPath = join2(consumerRoot, "vendor", vendorName);
|
|
746
|
+
const consumerPkgPath = join2(consumerRoot, "package.json");
|
|
614
747
|
const consumerPkg = await readPackageJson(consumerPkgPath);
|
|
615
748
|
let depMap;
|
|
616
749
|
let depSpecBefore;
|
|
@@ -626,7 +759,7 @@ async function eject(opts) {
|
|
|
626
759
|
`${pkg} is not a dependency of the consumer package.json`
|
|
627
760
|
);
|
|
628
761
|
}
|
|
629
|
-
if (
|
|
762
|
+
if (existsSync2(vendorPath)) {
|
|
630
763
|
if (!force) {
|
|
631
764
|
throw new EjectError(
|
|
632
765
|
`vendor/${vendorName} already exists; pass --force to overwrite`
|
|
@@ -654,7 +787,7 @@ async function eject(opts) {
|
|
|
654
787
|
}
|
|
655
788
|
});
|
|
656
789
|
copiedFiles = await countFiles(vendorPath);
|
|
657
|
-
const vendoredPkgPath =
|
|
790
|
+
const vendoredPkgPath = join2(vendorPath, "package.json");
|
|
658
791
|
const vendoredPkg = await readPackageJson(vendoredPkgPath);
|
|
659
792
|
if (vendoredPkg.private === true) {
|
|
660
793
|
delete vendoredPkg.private;
|
|
@@ -677,7 +810,7 @@ async function countFiles(dir) {
|
|
|
677
810
|
let count = 0;
|
|
678
811
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
679
812
|
for (const entry of entries) {
|
|
680
|
-
const full =
|
|
813
|
+
const full = join2(dir, entry.name);
|
|
681
814
|
if (entry.isDirectory()) {
|
|
682
815
|
count += await countFiles(full);
|
|
683
816
|
} else if (entry.isFile()) {
|
|
@@ -705,16 +838,16 @@ Options:
|
|
|
705
838
|
|
|
706
839
|
After ejecting, run: pnpm install`;
|
|
707
840
|
function resolveSourceDir(pkg, consumerRoot) {
|
|
708
|
-
const direct =
|
|
709
|
-
if (
|
|
841
|
+
const direct = join3(consumerRoot, "node_modules", pkg, "package.json");
|
|
842
|
+
if (existsSync3(direct)) {
|
|
710
843
|
return dirname(realpathSync(direct));
|
|
711
844
|
}
|
|
712
|
-
const require2 =
|
|
845
|
+
const require2 = createRequire2(`${consumerRoot}${sep2}`);
|
|
713
846
|
try {
|
|
714
847
|
const entry = require2.resolve(pkg);
|
|
715
848
|
let dir = dirname(entry);
|
|
716
849
|
while (dir !== dirname(dir)) {
|
|
717
|
-
if (
|
|
850
|
+
if (existsSync3(join3(dir, "package.json"))) return dir;
|
|
718
851
|
dir = dirname(dir);
|
|
719
852
|
}
|
|
720
853
|
} catch {
|
|
@@ -1213,8 +1346,8 @@ var patchCommand = {
|
|
|
1213
1346
|
// src/commands/setup.ts
|
|
1214
1347
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
1215
1348
|
import { randomBytes } from "crypto";
|
|
1216
|
-
import { copyFileSync, existsSync as
|
|
1217
|
-
import { join as
|
|
1349
|
+
import { copyFileSync, existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1350
|
+
import { join as join4 } from "path";
|
|
1218
1351
|
import { parseArgs as parseArgs7 } from "util";
|
|
1219
1352
|
import { confirm } from "@clack/prompts";
|
|
1220
1353
|
|
|
@@ -1252,16 +1385,16 @@ function generateSecret() {
|
|
|
1252
1385
|
var SECRET_KEY = "BETTER_AUTH_SECRET";
|
|
1253
1386
|
var PLACEHOLDER_PREFIX = "change-me";
|
|
1254
1387
|
function ensureEnv(cwd) {
|
|
1255
|
-
const envPath =
|
|
1256
|
-
const examplePath =
|
|
1388
|
+
const envPath = join4(cwd, ".env");
|
|
1389
|
+
const examplePath = join4(cwd, ".env.example");
|
|
1257
1390
|
let copied;
|
|
1258
|
-
if (
|
|
1391
|
+
if (existsSync4(envPath)) {
|
|
1259
1392
|
copied = {
|
|
1260
1393
|
step: "env",
|
|
1261
1394
|
status: "skipped",
|
|
1262
1395
|
detail: ".env already exists"
|
|
1263
1396
|
};
|
|
1264
|
-
} else if (
|
|
1397
|
+
} else if (existsSync4(examplePath)) {
|
|
1265
1398
|
copyFileSync(examplePath, envPath);
|
|
1266
1399
|
copied = {
|
|
1267
1400
|
step: "env",
|
|
@@ -1285,7 +1418,7 @@ function ensureEnv(cwd) {
|
|
|
1285
1418
|
}
|
|
1286
1419
|
let raw;
|
|
1287
1420
|
try {
|
|
1288
|
-
raw =
|
|
1421
|
+
raw = readFileSync2(envPath, "utf8");
|
|
1289
1422
|
} catch (err) {
|
|
1290
1423
|
return {
|
|
1291
1424
|
copied,
|
|
@@ -1321,7 +1454,7 @@ function ensureEnv(cwd) {
|
|
|
1321
1454
|
} else {
|
|
1322
1455
|
lines[idx] = newLine;
|
|
1323
1456
|
}
|
|
1324
|
-
|
|
1457
|
+
writeFileSync2(envPath, lines.join("\n"));
|
|
1325
1458
|
return {
|
|
1326
1459
|
copied,
|
|
1327
1460
|
secret: {
|
|
@@ -1355,12 +1488,12 @@ async function run7(ctx) {
|
|
|
1355
1488
|
return;
|
|
1356
1489
|
}
|
|
1357
1490
|
const cwd = values.cwd ?? process.cwd();
|
|
1358
|
-
if (!
|
|
1491
|
+
if (!existsSync4(join4(cwd, "package.json"))) {
|
|
1359
1492
|
ctx.out.fail(
|
|
1360
1493
|
`no package.json in ${cwd} \u2014 run setup from a scaffolded Hogsend app (or pass --cwd).`
|
|
1361
1494
|
);
|
|
1362
1495
|
}
|
|
1363
|
-
const hasCompose =
|
|
1496
|
+
const hasCompose = existsSync4(join4(cwd, "docker-compose.yml")) || existsSync4(join4(cwd, "docker-compose.yaml")) || existsSync4(join4(cwd, "compose.yml")) || existsSync4(join4(cwd, "compose.yaml"));
|
|
1364
1497
|
const skipConfirm = ctx.json || values.yes;
|
|
1365
1498
|
if (!ctx.json) {
|
|
1366
1499
|
ctx.out.intro(
|
|
@@ -1467,16 +1600,8 @@ var setupCommand = {
|
|
|
1467
1600
|
};
|
|
1468
1601
|
|
|
1469
1602
|
// src/commands/skills.ts
|
|
1470
|
-
import {
|
|
1471
|
-
|
|
1472
|
-
existsSync as existsSync4,
|
|
1473
|
-
mkdirSync,
|
|
1474
|
-
readdirSync,
|
|
1475
|
-
readFileSync as readFileSync2,
|
|
1476
|
-
statSync
|
|
1477
|
-
} from "fs";
|
|
1478
|
-
import { join as join4 } from "path";
|
|
1479
|
-
import { fileURLToPath } from "url";
|
|
1603
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1604
|
+
import { join as join5 } from "path";
|
|
1480
1605
|
import { parseArgs as parseArgs8 } from "util";
|
|
1481
1606
|
import { multiselect } from "@clack/prompts";
|
|
1482
1607
|
var usage8 = `hogsend skills <subcommand> [options]
|
|
@@ -1489,10 +1614,13 @@ Subcommands:
|
|
|
1489
1614
|
list List bundled skills + whether each is installed.
|
|
1490
1615
|
add [name] [--force] Copy a bundled skill into ./.claude/skills/<name>/.
|
|
1491
1616
|
Omit name for an interactive multiselect (human),
|
|
1492
|
-
or copy all bundled skills (--json /
|
|
1617
|
+
or copy all bundled skills (--all / --json /
|
|
1618
|
+
non-interactive).
|
|
1493
1619
|
|
|
1494
1620
|
Options:
|
|
1495
|
-
--
|
|
1621
|
+
--all Install every bundled skill (skips the interactive picker).
|
|
1622
|
+
--force Overwrite an already-installed skill. Use after upgrading the
|
|
1623
|
+
engine to refresh vendored skills to the latest guidance.
|
|
1496
1624
|
--json Emit machine-readable JSON only (implies non-interactive).
|
|
1497
1625
|
-h, --help Show this help.
|
|
1498
1626
|
|
|
@@ -1500,49 +1628,11 @@ Examples:
|
|
|
1500
1628
|
hogsend skills list
|
|
1501
1629
|
hogsend skills list --json
|
|
1502
1630
|
hogsend skills add
|
|
1503
|
-
hogsend skills add
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
return join4(cwd, ".claude", "skills");
|
|
1509
|
-
}
|
|
1510
|
-
function readFrontmatterField(skillDir, field) {
|
|
1511
|
-
const skillFile = join4(skillDir, "SKILL.md");
|
|
1512
|
-
if (!existsSync4(skillFile)) return "";
|
|
1513
|
-
const raw = readFileSyncSafe(skillFile);
|
|
1514
|
-
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
1515
|
-
if (!fmMatch) return "";
|
|
1516
|
-
const block = fmMatch[1] ?? "";
|
|
1517
|
-
for (const line of block.split("\n")) {
|
|
1518
|
-
const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
1519
|
-
if (m && m[1] === field) {
|
|
1520
|
-
return (m[2] ?? "").replace(/^["']|["']$/g, "").trim();
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
return "";
|
|
1524
|
-
}
|
|
1525
|
-
function readFileSyncSafe(path) {
|
|
1526
|
-
try {
|
|
1527
|
-
return readFileSync2(path, "utf8");
|
|
1528
|
-
} catch {
|
|
1529
|
-
return "";
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
function listBundledSkills(cwd) {
|
|
1533
|
-
const dir = bundledSkillsDir();
|
|
1534
|
-
if (!existsSync4(dir)) return [];
|
|
1535
|
-
const target = installDir(cwd);
|
|
1536
|
-
const entries = readdirSync(dir).filter((name) => {
|
|
1537
|
-
const full = join4(dir, name);
|
|
1538
|
-
return statSync(full).isDirectory() && existsSync4(join4(full, "SKILL.md"));
|
|
1539
|
-
});
|
|
1540
|
-
return entries.sort().map((name) => ({
|
|
1541
|
-
name,
|
|
1542
|
-
description: readFrontmatterField(join4(dir, name), "description"),
|
|
1543
|
-
installed: existsSync4(join4(target, name))
|
|
1544
|
-
}));
|
|
1545
|
-
}
|
|
1631
|
+
hogsend skills add --all
|
|
1632
|
+
hogsend skills add hogsend-cli --force
|
|
1633
|
+
hogsend skills add --all --force # refresh everything after an upgrade
|
|
1634
|
+
|
|
1635
|
+
Tip: \`hogsend upgrade\` bumps the engine AND refreshes these skills in one step.`;
|
|
1546
1636
|
function runList3(ctx) {
|
|
1547
1637
|
const skills = listBundledSkills(process.cwd());
|
|
1548
1638
|
if (ctx.json) {
|
|
@@ -1571,25 +1661,15 @@ function runList3(ctx) {
|
|
|
1571
1661
|
["name", "installed", "description"]
|
|
1572
1662
|
);
|
|
1573
1663
|
ctx.out.outro(
|
|
1574
|
-
`Install with ${color.cyan("hogsend skills add <name>")} (or
|
|
1664
|
+
`Install with ${color.cyan("hogsend skills add <name>")} (or ${color.cyan("hogsend skills add --all")}). Refresh after an engine upgrade with ${color.cyan("--force")}.`
|
|
1575
1665
|
);
|
|
1576
1666
|
}
|
|
1577
|
-
function copySkill(name, cwd, force) {
|
|
1578
|
-
const src = join4(bundledSkillsDir(), name);
|
|
1579
|
-
const dest = join4(installDir(cwd), name);
|
|
1580
|
-
const exists = existsSync4(dest);
|
|
1581
|
-
if (exists && !force) {
|
|
1582
|
-
return { name, installed: false, skipped: true, path: dest };
|
|
1583
|
-
}
|
|
1584
|
-
mkdirSync(installDir(cwd), { recursive: true });
|
|
1585
|
-
cpSync(src, dest, { recursive: true, force: true });
|
|
1586
|
-
return { name, installed: true, skipped: false, path: dest };
|
|
1587
|
-
}
|
|
1588
1667
|
async function runAdd(ctx, argv) {
|
|
1589
1668
|
const { values, positionals } = parseArgs8({
|
|
1590
1669
|
args: argv,
|
|
1591
1670
|
allowPositionals: true,
|
|
1592
1671
|
options: {
|
|
1672
|
+
all: { type: "boolean", default: false },
|
|
1593
1673
|
force: { type: "boolean", default: false },
|
|
1594
1674
|
help: { type: "boolean", short: "h", default: false }
|
|
1595
1675
|
}
|
|
@@ -1614,6 +1694,8 @@ async function runAdd(ctx, argv) {
|
|
|
1614
1694
|
);
|
|
1615
1695
|
}
|
|
1616
1696
|
names = [requested];
|
|
1697
|
+
} else if (values.all) {
|
|
1698
|
+
names = bundled.map((s) => s.name);
|
|
1617
1699
|
} else if (ctx.out.interactive) {
|
|
1618
1700
|
const picked = bail(
|
|
1619
1701
|
await multiselect({
|
|
@@ -1630,7 +1712,13 @@ async function runAdd(ctx, argv) {
|
|
|
1630
1712
|
} else {
|
|
1631
1713
|
names = bundled.map((s) => s.name);
|
|
1632
1714
|
}
|
|
1633
|
-
const results = names.map(
|
|
1715
|
+
const results = names.map(
|
|
1716
|
+
(name) => copySkill(name, cwd, force)
|
|
1717
|
+
);
|
|
1718
|
+
if (results.some((r) => r.installed)) {
|
|
1719
|
+
const installedNames = listBundledSkills(cwd).filter((s) => existsSync5(join5(installDir(cwd), s.name))).map((s) => s.name);
|
|
1720
|
+
writeSkillsStamp(cwd, installedNames);
|
|
1721
|
+
}
|
|
1634
1722
|
if (ctx.json) {
|
|
1635
1723
|
ctx.out.json({
|
|
1636
1724
|
installDir: installDir(cwd),
|
|
@@ -1748,6 +1836,387 @@ var statsCommand = {
|
|
|
1748
1836
|
run: run9
|
|
1749
1837
|
};
|
|
1750
1838
|
|
|
1839
|
+
// src/commands/studio.ts
|
|
1840
|
+
import { spawn } from "child_process";
|
|
1841
|
+
import { createReadStream, existsSync as existsSync6, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
1842
|
+
import { createServer } from "http";
|
|
1843
|
+
import { extname, join as join6, normalize, resolve, sep as sep3 } from "path";
|
|
1844
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1845
|
+
import { parseArgs as parseArgs10 } from "util";
|
|
1846
|
+
var usage10 = `hogsend studio [options]
|
|
1847
|
+
|
|
1848
|
+
Serve the bundled Hogsend Studio (the admin SPA) locally and open it in a
|
|
1849
|
+
browser. The Studio is a static single-page app; this command starts a tiny
|
|
1850
|
+
local web server for it on a port of your choosing.
|
|
1851
|
+
|
|
1852
|
+
By default the Studio talks to the API at the same origin it is served from,
|
|
1853
|
+
which won't be a running API here \u2014 so point it at your instance with
|
|
1854
|
+
--base-url (the SPA uses cookie auth, so the instance must allow CORS from the
|
|
1855
|
+
Studio origin, or you can simply open the Studio that the engine mounts at
|
|
1856
|
+
\`<instance>/studio\` instead).
|
|
1857
|
+
|
|
1858
|
+
Options:
|
|
1859
|
+
--port <n> Local port to serve on (default 3333).
|
|
1860
|
+
--base-url <url> API instance the Studio should call (injected at runtime).
|
|
1861
|
+
Omit to use same-origin (the local server, for static
|
|
1862
|
+
preview only).
|
|
1863
|
+
--open Open the Studio in your default browser after starting.
|
|
1864
|
+
--dist <path> Override the Studio dist directory (advanced).
|
|
1865
|
+
-h, --help Show this help.
|
|
1866
|
+
|
|
1867
|
+
Examples:
|
|
1868
|
+
hogsend studio --open
|
|
1869
|
+
hogsend studio --base-url https://api.example.com --open
|
|
1870
|
+
hogsend studio --port 4000`;
|
|
1871
|
+
function resolveStudioDist(distFlag) {
|
|
1872
|
+
const candidates = [];
|
|
1873
|
+
if (distFlag && distFlag.length > 0) {
|
|
1874
|
+
candidates.push(resolve(process.cwd(), distFlag));
|
|
1875
|
+
}
|
|
1876
|
+
candidates.push(fileURLToPath2(new URL("../studio", import.meta.url)));
|
|
1877
|
+
candidates.push(
|
|
1878
|
+
fileURLToPath2(new URL("../../studio/dist", import.meta.url)),
|
|
1879
|
+
fileURLToPath2(new URL("../../../studio/dist", import.meta.url))
|
|
1880
|
+
);
|
|
1881
|
+
candidates.push(resolve(process.cwd(), "packages/studio/dist"));
|
|
1882
|
+
for (const dir of candidates) {
|
|
1883
|
+
if (existsSync6(join6(dir, "index.html"))) {
|
|
1884
|
+
return dir;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
return null;
|
|
1888
|
+
}
|
|
1889
|
+
var MIME = {
|
|
1890
|
+
".html": "text/html; charset=utf-8",
|
|
1891
|
+
".js": "text/javascript; charset=utf-8",
|
|
1892
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
1893
|
+
".css": "text/css; charset=utf-8",
|
|
1894
|
+
".json": "application/json; charset=utf-8",
|
|
1895
|
+
".svg": "image/svg+xml",
|
|
1896
|
+
".png": "image/png",
|
|
1897
|
+
".jpg": "image/jpeg",
|
|
1898
|
+
".jpeg": "image/jpeg",
|
|
1899
|
+
".gif": "image/gif",
|
|
1900
|
+
".ico": "image/x-icon",
|
|
1901
|
+
".woff": "font/woff",
|
|
1902
|
+
".woff2": "font/woff2",
|
|
1903
|
+
".ttf": "font/ttf",
|
|
1904
|
+
".map": "application/json; charset=utf-8"
|
|
1905
|
+
};
|
|
1906
|
+
function mimeFor(path) {
|
|
1907
|
+
return MIME[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
1908
|
+
}
|
|
1909
|
+
function indexHtml(distPath, baseUrl) {
|
|
1910
|
+
const raw = readFileSync3(join6(distPath, "index.html"), "utf8");
|
|
1911
|
+
if (!baseUrl) return raw;
|
|
1912
|
+
const inject = `<script>window.__HOGSEND_STUDIO__=${JSON.stringify({
|
|
1913
|
+
baseUrl
|
|
1914
|
+
})};</script>`;
|
|
1915
|
+
if (raw.includes("</head>")) {
|
|
1916
|
+
return raw.replace("</head>", `${inject}</head>`);
|
|
1917
|
+
}
|
|
1918
|
+
return `${inject}${raw}`;
|
|
1919
|
+
}
|
|
1920
|
+
function openBrowser(url) {
|
|
1921
|
+
const platform = process.platform;
|
|
1922
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
1923
|
+
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
1924
|
+
try {
|
|
1925
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
1926
|
+
child.on("error", () => {
|
|
1927
|
+
});
|
|
1928
|
+
child.unref();
|
|
1929
|
+
} catch {
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
async function run10(ctx) {
|
|
1933
|
+
const { values, positionals } = parseArgs10({
|
|
1934
|
+
args: ctx.argv,
|
|
1935
|
+
allowPositionals: true,
|
|
1936
|
+
strict: false,
|
|
1937
|
+
options: {
|
|
1938
|
+
port: { type: "string" },
|
|
1939
|
+
"base-url": { type: "string" },
|
|
1940
|
+
open: { type: "boolean", default: false },
|
|
1941
|
+
dist: { type: "string" },
|
|
1942
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
1945
|
+
if (values.help) {
|
|
1946
|
+
ctx.out.log(usage10);
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
const port = Number(values.port ?? "3333");
|
|
1950
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
1951
|
+
ctx.out.fail(`invalid --port "${values.port}" (expected 1-65535)`);
|
|
1952
|
+
}
|
|
1953
|
+
const baseUrl = typeof values["base-url"] === "string" ? values["base-url"] : void 0;
|
|
1954
|
+
const distPath = resolveStudioDist(
|
|
1955
|
+
typeof values.dist === "string" ? values.dist : positionals[0]
|
|
1956
|
+
);
|
|
1957
|
+
if (!distPath) {
|
|
1958
|
+
ctx.out.fail(
|
|
1959
|
+
"could not find a built Studio (dist/). Build it with `pnpm --filter @hogsend/studio build`, or pass --dist <path>."
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
const cleanBase = baseUrl ? baseUrl.replace(/\/+$/, "") : void 0;
|
|
1963
|
+
const index = indexHtml(distPath, cleanBase);
|
|
1964
|
+
const server = createServer((req, res) => {
|
|
1965
|
+
const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0] ?? "/");
|
|
1966
|
+
const rel = urlPath.replace(/^\/studio/, "");
|
|
1967
|
+
if (rel === "" || rel === "/") {
|
|
1968
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
1969
|
+
res.end(index);
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
const target = normalize(join6(distPath, rel));
|
|
1973
|
+
if (target !== distPath && !target.startsWith(distPath + sep3)) {
|
|
1974
|
+
res.writeHead(403);
|
|
1975
|
+
res.end("Forbidden");
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
if (existsSync6(target) && statSync2(target).isFile()) {
|
|
1979
|
+
res.writeHead(200, { "content-type": mimeFor(target) });
|
|
1980
|
+
createReadStream(target).pipe(res);
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
1984
|
+
res.end(index);
|
|
1985
|
+
});
|
|
1986
|
+
await new Promise((resolveListen, reject) => {
|
|
1987
|
+
server.once("error", reject);
|
|
1988
|
+
server.listen(port, () => resolveListen());
|
|
1989
|
+
}).catch((err) => {
|
|
1990
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1991
|
+
ctx.out.fail(`could not start server on port ${port}: ${msg}`);
|
|
1992
|
+
});
|
|
1993
|
+
const localUrl = `http://localhost:${port}/studio/`;
|
|
1994
|
+
if (ctx.json) {
|
|
1995
|
+
ctx.out.json({
|
|
1996
|
+
url: localUrl,
|
|
1997
|
+
port,
|
|
1998
|
+
baseUrl: cleanBase ?? null,
|
|
1999
|
+
dist: distPath
|
|
2000
|
+
});
|
|
2001
|
+
} else {
|
|
2002
|
+
ctx.out.intro(`${color.bgMagenta(color.black(" hogsend "))} studio`);
|
|
2003
|
+
ctx.out.note(
|
|
2004
|
+
[
|
|
2005
|
+
`${color.green("\u25CF")} Studio serving at ${color.cyan(localUrl)}`,
|
|
2006
|
+
cleanBase ? color.dim(`API instance: ${cleanBase}`) : color.dim(
|
|
2007
|
+
"No --base-url set (same-origin / static preview). The API calls will hit this local server and fail \u2014 pass --base-url <instance>, or open <instance>/studio directly."
|
|
2008
|
+
),
|
|
2009
|
+
"",
|
|
2010
|
+
color.dim("First load shows a create-admin screen if no admin exists."),
|
|
2011
|
+
color.dim("Press Ctrl+C to stop.")
|
|
2012
|
+
].join("\n"),
|
|
2013
|
+
"Studio"
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
if (values.open) {
|
|
2017
|
+
openBrowser(localUrl);
|
|
2018
|
+
}
|
|
2019
|
+
await new Promise((resolveForever) => {
|
|
2020
|
+
const stop = () => {
|
|
2021
|
+
server.close(() => resolveForever());
|
|
2022
|
+
};
|
|
2023
|
+
process.on("SIGINT", stop);
|
|
2024
|
+
process.on("SIGTERM", stop);
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
var studioCommand = {
|
|
2028
|
+
name: "studio",
|
|
2029
|
+
summary: "Serve the bundled Hogsend Studio admin SPA locally",
|
|
2030
|
+
usage: usage10,
|
|
2031
|
+
run: run10
|
|
2032
|
+
};
|
|
2033
|
+
|
|
2034
|
+
// src/commands/upgrade.ts
|
|
2035
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
2036
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
|
|
2037
|
+
import { join as join7 } from "path";
|
|
2038
|
+
import { parseArgs as parseArgs11 } from "util";
|
|
2039
|
+
import { confirm as confirm2 } from "@clack/prompts";
|
|
2040
|
+
var usage11 = `hogsend upgrade [--cwd <dir>] [--pm <pnpm|npm|yarn|bun>] [options]
|
|
2041
|
+
|
|
2042
|
+
Upgrade a scaffolded Hogsend app in one step:
|
|
2043
|
+
1. bump every @hogsend/* dependency to latest (or --to <version>), then
|
|
2044
|
+
2. refresh the vendored Claude Code skills in ./.claude/skills to match.
|
|
2045
|
+
|
|
2046
|
+
Run this after a new engine release so your app AND the agent guidance move
|
|
2047
|
+
together. Skills are version-stamped so \`hogsend doctor\` can warn when they
|
|
2048
|
+
fall behind.
|
|
2049
|
+
|
|
2050
|
+
Options:
|
|
2051
|
+
--cwd <dir> Project root to upgrade (defaults to the current directory).
|
|
2052
|
+
--pm <manager> Package manager (default: detected from the lockfile, else pnpm).
|
|
2053
|
+
--to <version> Target version for @hogsend/* deps (default: latest).
|
|
2054
|
+
--deps-only Bump dependencies only; don't touch skills.
|
|
2055
|
+
--skills-only Refresh skills only; don't touch dependencies.
|
|
2056
|
+
--yes, -y Skip the confirmation prompt. Implied by --json.
|
|
2057
|
+
--json Run non-interactively and emit a single JSON result.
|
|
2058
|
+
-h, --help Show this help.`;
|
|
2059
|
+
var VALID_PMS = ["pnpm", "npm", "yarn", "bun"];
|
|
2060
|
+
function detectPm(cwd) {
|
|
2061
|
+
if (existsSync7(join7(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
2062
|
+
if (existsSync7(join7(cwd, "yarn.lock"))) return "yarn";
|
|
2063
|
+
if (existsSync7(join7(cwd, "bun.lockb")) || existsSync7(join7(cwd, "bun.lock")))
|
|
2064
|
+
return "bun";
|
|
2065
|
+
if (existsSync7(join7(cwd, "package-lock.json"))) return "npm";
|
|
2066
|
+
return "pnpm";
|
|
2067
|
+
}
|
|
2068
|
+
function hogsendDeps(cwd) {
|
|
2069
|
+
const pkg = JSON.parse(readFileSync4(join7(cwd, "package.json"), "utf8"));
|
|
2070
|
+
const all = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2071
|
+
return Object.keys(all).filter((n) => n.startsWith("@hogsend/")).sort();
|
|
2072
|
+
}
|
|
2073
|
+
function addArgs(pm, specs) {
|
|
2074
|
+
return [pm === "npm" ? "install" : "add", ...specs];
|
|
2075
|
+
}
|
|
2076
|
+
async function run11(ctx) {
|
|
2077
|
+
const { values } = parseArgs11({
|
|
2078
|
+
args: ctx.argv,
|
|
2079
|
+
allowPositionals: true,
|
|
2080
|
+
options: {
|
|
2081
|
+
cwd: { type: "string" },
|
|
2082
|
+
pm: { type: "string" },
|
|
2083
|
+
to: { type: "string" },
|
|
2084
|
+
"deps-only": { type: "boolean", default: false },
|
|
2085
|
+
"skills-only": { type: "boolean", default: false },
|
|
2086
|
+
yes: { type: "boolean", short: "y", default: false },
|
|
2087
|
+
help: { type: "boolean", short: "h", default: false }
|
|
2088
|
+
}
|
|
2089
|
+
});
|
|
2090
|
+
if (values.help) {
|
|
2091
|
+
ctx.out.log(usage11);
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
if (values["deps-only"] && values["skills-only"]) {
|
|
2095
|
+
ctx.out.fail("--deps-only and --skills-only are mutually exclusive.");
|
|
2096
|
+
}
|
|
2097
|
+
const cwd = values.cwd ?? process.cwd();
|
|
2098
|
+
if (!existsSync7(join7(cwd, "package.json"))) {
|
|
2099
|
+
ctx.out.fail(
|
|
2100
|
+
`no package.json in ${cwd} \u2014 run upgrade from a scaffolded Hogsend app (or pass --cwd).`
|
|
2101
|
+
);
|
|
2102
|
+
}
|
|
2103
|
+
let pm;
|
|
2104
|
+
if (values.pm !== void 0) {
|
|
2105
|
+
if (!VALID_PMS.includes(values.pm)) {
|
|
2106
|
+
ctx.out.fail(
|
|
2107
|
+
`invalid --pm "${values.pm}". Expected one of: ${VALID_PMS.join(", ")}.`
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
pm = values.pm;
|
|
2111
|
+
} else {
|
|
2112
|
+
pm = detectPm(cwd);
|
|
2113
|
+
}
|
|
2114
|
+
const target = values.to ?? "latest";
|
|
2115
|
+
const doDeps = !values["skills-only"];
|
|
2116
|
+
const doSkills = !values["deps-only"];
|
|
2117
|
+
const deps = doDeps ? hogsendDeps(cwd) : [];
|
|
2118
|
+
if (doDeps && deps.length === 0) {
|
|
2119
|
+
ctx.out.fail(
|
|
2120
|
+
`no @hogsend/* dependencies found in ${join7(cwd, "package.json")}.`
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
const skipConfirm = ctx.json || values.yes;
|
|
2124
|
+
if (!ctx.json) {
|
|
2125
|
+
ctx.out.intro(
|
|
2126
|
+
`${color.bgMagenta(color.black(" hogsend "))} ${color.dim("upgrade")}`
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
if (ctx.out.interactive && !skipConfirm) {
|
|
2130
|
+
const plan = [
|
|
2131
|
+
doDeps ? `bump ${deps.length} @hogsend/* dep(s) to ${target} (${pm})` : null,
|
|
2132
|
+
doSkills ? "refresh .claude/skills" : null
|
|
2133
|
+
].filter(Boolean).join(" + ");
|
|
2134
|
+
const proceed = bail(
|
|
2135
|
+
await confirm2({ message: `Upgrade ${color.cyan(cwd)}: ${plan}?` })
|
|
2136
|
+
);
|
|
2137
|
+
if (!proceed) {
|
|
2138
|
+
ctx.out.outro(color.dim("Nothing changed."));
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
const results = [];
|
|
2143
|
+
if (doDeps) {
|
|
2144
|
+
const specs = deps.map((n) => `${n}@${target}`);
|
|
2145
|
+
const dep = await ctx.out.step(
|
|
2146
|
+
`Bumping @hogsend/* -> ${target} (${pm})`,
|
|
2147
|
+
async () => spawnSync3(pm, addArgs(pm, specs), {
|
|
2148
|
+
cwd,
|
|
2149
|
+
stdio: ctx.json ? "ignore" : "inherit",
|
|
2150
|
+
shell: process.platform === "win32"
|
|
2151
|
+
})
|
|
2152
|
+
);
|
|
2153
|
+
results.push({
|
|
2154
|
+
step: "deps",
|
|
2155
|
+
status: dep.status === 0 ? "ok" : "failed",
|
|
2156
|
+
detail: dep.status === 0 ? `${deps.join(", ")} -> ${target}` : `${pm} exited with code ${dep.status ?? "?"}`
|
|
2157
|
+
});
|
|
2158
|
+
} else {
|
|
2159
|
+
results.push({ step: "deps", status: "skipped", detail: "--skills-only" });
|
|
2160
|
+
}
|
|
2161
|
+
const depsFailed = results.some(
|
|
2162
|
+
(r) => r.step === "deps" && r.status === "failed"
|
|
2163
|
+
);
|
|
2164
|
+
if (!doSkills) {
|
|
2165
|
+
results.push({
|
|
2166
|
+
step: "skills",
|
|
2167
|
+
status: "skipped",
|
|
2168
|
+
detail: "--deps-only"
|
|
2169
|
+
});
|
|
2170
|
+
} else if (depsFailed) {
|
|
2171
|
+
results.push({
|
|
2172
|
+
step: "skills",
|
|
2173
|
+
status: "skipped",
|
|
2174
|
+
detail: "skipped \u2014 dependency bump failed; fix it then re-run"
|
|
2175
|
+
});
|
|
2176
|
+
} else {
|
|
2177
|
+
const bundled = listBundledSkills(cwd);
|
|
2178
|
+
const copied = bundled.map((s) => copySkill(s.name, cwd, true));
|
|
2179
|
+
writeSkillsStamp(
|
|
2180
|
+
cwd,
|
|
2181
|
+
bundled.map((s) => s.name)
|
|
2182
|
+
);
|
|
2183
|
+
results.push({
|
|
2184
|
+
step: "skills",
|
|
2185
|
+
status: "ok",
|
|
2186
|
+
detail: `refreshed ${copied.length} skill(s) -> ${installDir(cwd)}`
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
const failed = results.filter((r) => r.status === "failed");
|
|
2190
|
+
const ok = failed.length === 0;
|
|
2191
|
+
if (ctx.json) {
|
|
2192
|
+
ctx.out.json({ ok, cwd, pm, target, steps: results });
|
|
2193
|
+
if (!ok) process.exit(1);
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
ctx.out.table(
|
|
2197
|
+
results.map((r) => ({
|
|
2198
|
+
step: r.step,
|
|
2199
|
+
status: r.status === "ok" ? color.green("ok") : r.status === "skipped" ? color.dim("skipped") : color.red("failed"),
|
|
2200
|
+
detail: r.detail
|
|
2201
|
+
})),
|
|
2202
|
+
["step", "status", "detail"]
|
|
2203
|
+
);
|
|
2204
|
+
if (!ok) {
|
|
2205
|
+
ctx.out.fail(
|
|
2206
|
+
`${failed.length} step(s) failed \u2014 see the table above. Fix and re-run hogsend upgrade.`
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2209
|
+
ctx.out.outro(
|
|
2210
|
+
`${color.green("Upgraded.")} ${color.dim("Engine + agent skills are on the latest line.")}`
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2213
|
+
var upgradeCommand = {
|
|
2214
|
+
name: "upgrade",
|
|
2215
|
+
summary: "Bump @hogsend/* deps to latest + refresh vendored skills",
|
|
2216
|
+
usage: usage11,
|
|
2217
|
+
run: run11
|
|
2218
|
+
};
|
|
2219
|
+
|
|
1751
2220
|
// src/commands/index.ts
|
|
1752
2221
|
var commands = [
|
|
1753
2222
|
doctorCommand,
|
|
@@ -1755,19 +2224,21 @@ var commands = [
|
|
|
1755
2224
|
contactsCommand,
|
|
1756
2225
|
statsCommand,
|
|
1757
2226
|
eventsCommand,
|
|
2227
|
+
studioCommand,
|
|
1758
2228
|
setupCommand,
|
|
1759
2229
|
skillsCommand,
|
|
2230
|
+
upgradeCommand,
|
|
1760
2231
|
ejectCommand,
|
|
1761
2232
|
patchCommand
|
|
1762
2233
|
];
|
|
1763
2234
|
|
|
1764
2235
|
// src/lib/config.ts
|
|
1765
|
-
import { existsSync as
|
|
1766
|
-
import { join as
|
|
1767
|
-
import { parseArgs as
|
|
2236
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
|
|
2237
|
+
import { join as join8 } from "path";
|
|
2238
|
+
import { parseArgs as parseArgs12 } from "util";
|
|
1768
2239
|
var DEFAULT_BASE_URL = "http://localhost:3002";
|
|
1769
2240
|
function parseGlobalFlags(argv) {
|
|
1770
|
-
const { values, tokens } =
|
|
2241
|
+
const { values, tokens } = parseArgs12({
|
|
1771
2242
|
args: argv,
|
|
1772
2243
|
allowPositionals: true,
|
|
1773
2244
|
strict: false,
|
|
@@ -1804,11 +2275,11 @@ function parseGlobalFlags(argv) {
|
|
|
1804
2275
|
}
|
|
1805
2276
|
function loadDotEnv(cwd = process.cwd()) {
|
|
1806
2277
|
const out = {};
|
|
1807
|
-
const file =
|
|
1808
|
-
if (!
|
|
2278
|
+
const file = join8(cwd, ".env");
|
|
2279
|
+
if (!existsSync8(file)) return out;
|
|
1809
2280
|
let raw;
|
|
1810
2281
|
try {
|
|
1811
|
-
raw =
|
|
2282
|
+
raw = readFileSync5(file, "utf8");
|
|
1812
2283
|
} catch {
|
|
1813
2284
|
return out;
|
|
1814
2285
|
}
|
|
@@ -1841,7 +2312,7 @@ function resolveConfig(flags, cwd = process.cwd()) {
|
|
|
1841
2312
|
// src/bin.ts
|
|
1842
2313
|
function version() {
|
|
1843
2314
|
try {
|
|
1844
|
-
const require2 =
|
|
2315
|
+
const require2 = createRequire3(import.meta.url);
|
|
1845
2316
|
const pkg = require2("../package.json");
|
|
1846
2317
|
return pkg.version ?? "0.0.0";
|
|
1847
2318
|
} catch {
|