@veleads/gtm 0.2.1 → 0.3.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/CHANGELOG.md +12 -0
- package/README.md +16 -16
- package/dist/cli.js +143 -93
- package/kit/AGENTS.md +63 -63
- package/kit/providers/_template.md +1 -1
- package/kit/schemas/account.schema.json +2 -2
- package/kit/schemas/campaign-config.schema.json +3 -3
- package/kit/schemas/contact.schema.json +2 -2
- package/kit/schemas/icp.schema.json +3 -3
- package/kit/schemas/lead.schema.json +2 -2
- package/kit/schemas/{client.schema.json → project.schema.json} +7 -7
- package/kit/schemas/signal.schema.json +2 -2
- package/kit/schemas/signals.schema.json +3 -3
- package/kit/skills/00-intake/SKILL.md +12 -12
- package/kit/skills/01-icp/SKILL.md +9 -9
- package/kit/skills/02-tam/SKILL.md +5 -5
- package/kit/skills/03-enrich/SKILL.md +1 -1
- package/kit/skills/04-qualify/SKILL.md +4 -4
- package/kit/skills/05-copy/SKILL.md +6 -6
- package/kit/skills/06-qa/SKILL.md +2 -2
- package/kit/skills/07-launch/SKILL.md +3 -3
- package/kit/skills/08-results/SKILL.md +4 -4
- package/kit/templates/campaign/brief.md +1 -1
- package/kit/templates/campaign/config.yaml +3 -3
- package/kit/templates/outputs/qa-report.md +1 -1
- package/kit/templates/outputs/run-log.md +1 -1
- package/kit/templates/outputs/weekly-report.md +1 -1
- package/kit/templates/{client → project}/icp.md +1 -1
- package/kit/templates/{client → project}/icp.yaml +2 -2
- package/kit/templates/{client → project}/intake.md +11 -11
- package/kit/templates/{client → project}/offer.md +1 -1
- package/kit/templates/{client → project}/personas.md +1 -1
- package/kit/templates/project/project.yaml +12 -0
- package/kit/templates/{client → project}/proof.md +1 -1
- package/kit/templates/{client → project}/signals.yaml +2 -2
- package/kit/templates/{client → project}/voice.md +1 -1
- package/kit/templates/repo/{CLIENT.md → PROJECT.md} +5 -5
- package/kit/templates/repo/README.md +9 -9
- package/kit/templates/repo/github/workflows/gtm-upgrade.yml +10 -12
- package/kit/templates/repo/github/workflows/gtm.yml +14 -8
- package/kit/templates/repo/package.json +1 -2
- package/package.json +1 -1
- package/kit/templates/client/client.yaml +0 -12
package/dist/cli.js
CHANGED
|
@@ -40,28 +40,32 @@ var Approval = z.object({
|
|
|
40
40
|
);
|
|
41
41
|
var FieldSources = z.record(z.string(), z.string().min(1));
|
|
42
42
|
|
|
43
|
-
// ../schemas/src/
|
|
43
|
+
// ../schemas/src/project.ts
|
|
44
44
|
import { z as z2 } from "zod";
|
|
45
45
|
|
|
46
46
|
// ../schemas/src/versions.ts
|
|
47
47
|
var FILE_VERSIONS = {
|
|
48
|
-
|
|
49
|
-
icp:
|
|
50
|
-
signals:
|
|
51
|
-
"campaign-config":
|
|
48
|
+
project: 2,
|
|
49
|
+
icp: 2,
|
|
50
|
+
signals: 2,
|
|
51
|
+
"campaign-config": 2
|
|
52
52
|
};
|
|
53
|
+
var FILE_RENAMES = [
|
|
54
|
+
{ from: "client.yaml", to: "project.yaml", since: "0.3.0" },
|
|
55
|
+
{ from: "CLIENT.md", to: "PROJECT.md", since: "0.3.0" }
|
|
56
|
+
];
|
|
53
57
|
|
|
54
|
-
// ../schemas/src/
|
|
55
|
-
var
|
|
56
|
-
version: z2.literal(FILE_VERSIONS.
|
|
57
|
-
|
|
58
|
+
// ../schemas/src/project.ts
|
|
59
|
+
var ProjectFile = z2.object({
|
|
60
|
+
version: z2.literal(FILE_VERSIONS.project),
|
|
61
|
+
project_id: Slug,
|
|
58
62
|
name: z2.string().min(1),
|
|
59
|
-
status: z2.enum(["onboarding", "active", "paused", "
|
|
63
|
+
status: z2.enum(["onboarding", "active", "paused", "closed"]),
|
|
60
64
|
started_at: IsoDate,
|
|
61
65
|
owners: z2.object({
|
|
62
66
|
gtm_engineer: z2.string().min(1),
|
|
63
67
|
sdrs: z2.array(z2.string().min(1)),
|
|
64
|
-
|
|
68
|
+
approver: z2.string().min(1).describe("Person who signs off ICP and copy for this project")
|
|
65
69
|
}),
|
|
66
70
|
regions: z2.array(z2.string().min(2)).min(1).describe("Target regions, e.g. EU, UK, US"),
|
|
67
71
|
features: z2.record(z2.string(), z2.boolean()).default({}).describe("Opt-in core features, see FEATURES")
|
|
@@ -94,7 +98,7 @@ var IcpPersona = z3.object({
|
|
|
94
98
|
});
|
|
95
99
|
var IcpFile = z3.object({
|
|
96
100
|
version: z3.literal(FILE_VERSIONS.icp),
|
|
97
|
-
|
|
101
|
+
project_id: Slug,
|
|
98
102
|
approval: Approval,
|
|
99
103
|
segments: z3.array(IcpSegment).min(1),
|
|
100
104
|
personas: z3.array(IcpPersona).min(1),
|
|
@@ -124,7 +128,7 @@ var SignalDefinition = z4.object({
|
|
|
124
128
|
});
|
|
125
129
|
var SignalsFile = z4.object({
|
|
126
130
|
version: z4.literal(FILE_VERSIONS.signals),
|
|
127
|
-
|
|
131
|
+
project_id: Slug,
|
|
128
132
|
signals: z4.array(SignalDefinition).min(1)
|
|
129
133
|
}).superRefine((f, ctx) => {
|
|
130
134
|
const keys = f.signals.map((s) => s.key);
|
|
@@ -185,7 +189,7 @@ var Compliance = z5.object({
|
|
|
185
189
|
});
|
|
186
190
|
var CampaignConfig = z5.object({
|
|
187
191
|
version: z5.literal(FILE_VERSIONS["campaign-config"]),
|
|
188
|
-
|
|
192
|
+
project_id: Slug,
|
|
189
193
|
campaign_id: Slug,
|
|
190
194
|
owner: z5.string().min(1),
|
|
191
195
|
stage: Stage.describe("The stage currently being worked on"),
|
|
@@ -208,7 +212,7 @@ var CampaignConfig = z5.object({
|
|
|
208
212
|
// ../schemas/src/account.ts
|
|
209
213
|
import { z as z6 } from "zod";
|
|
210
214
|
var Account = z6.object({
|
|
211
|
-
|
|
215
|
+
project_id: Slug,
|
|
212
216
|
tam_id: Slug,
|
|
213
217
|
domain: Domain,
|
|
214
218
|
name: z6.string().min(1),
|
|
@@ -225,7 +229,7 @@ var Account = z6.object({
|
|
|
225
229
|
import { z as z7 } from "zod";
|
|
226
230
|
var EmailStatus = z7.enum(["valid", "catch_all", "invalid", "unknown", "not_found"]);
|
|
227
231
|
var Contact = z7.object({
|
|
228
|
-
|
|
232
|
+
project_id: Slug,
|
|
229
233
|
account_domain: Domain,
|
|
230
234
|
first_name: z7.string().min(1),
|
|
231
235
|
last_name: z7.string().min(1),
|
|
@@ -242,7 +246,7 @@ var Contact = z7.object({
|
|
|
242
246
|
// ../schemas/src/signal.ts
|
|
243
247
|
import { z as z8 } from "zod";
|
|
244
248
|
var SignalEvent = z8.object({
|
|
245
|
-
|
|
249
|
+
project_id: Slug,
|
|
246
250
|
signal_key: Slug,
|
|
247
251
|
account_domain: Domain,
|
|
248
252
|
contact_linkedin_url: z8.string().url().nullable(),
|
|
@@ -255,7 +259,7 @@ var SignalEvent = z8.object({
|
|
|
255
259
|
// ../schemas/src/lead.ts
|
|
256
260
|
import { z as z9 } from "zod";
|
|
257
261
|
var Lead = z9.object({
|
|
258
|
-
|
|
262
|
+
project_id: Slug,
|
|
259
263
|
campaign_id: Slug,
|
|
260
264
|
run_id: z9.uuid(),
|
|
261
265
|
email: z9.email(),
|
|
@@ -282,7 +286,27 @@ var FEATURES = {
|
|
|
282
286
|
};
|
|
283
287
|
|
|
284
288
|
// ../schemas/src/migrations.ts
|
|
285
|
-
|
|
289
|
+
import { isMap, isScalar } from "yaml";
|
|
290
|
+
function renameKey(doc, path, from, to) {
|
|
291
|
+
const node = path.length ? doc.getIn(path, true) : doc.contents;
|
|
292
|
+
if (!isMap(node)) return;
|
|
293
|
+
const pair = node.items.find((p) => (isScalar(p.key) ? p.key.value : p.key) === from);
|
|
294
|
+
if (!pair) return;
|
|
295
|
+
if (isScalar(pair.key)) pair.key.value = to;
|
|
296
|
+
else pair.key = to;
|
|
297
|
+
}
|
|
298
|
+
var renameClientId = (doc) => renameKey(doc, [], "client_id", "project_id");
|
|
299
|
+
var MIGRATIONS = [
|
|
300
|
+
// 0.3.0: "clients" became "projects" (one private repo per project).
|
|
301
|
+
{ kind: "project", from: 1, description: "client_id \u2192 project_id, owners.client_approver \u2192 owners.approver, status churned \u2192 closed", up(doc) {
|
|
302
|
+
renameClientId(doc);
|
|
303
|
+
renameKey(doc, ["owners"], "client_approver", "approver");
|
|
304
|
+
if (doc.get("status") === "churned") doc.set("status", "closed");
|
|
305
|
+
} },
|
|
306
|
+
{ kind: "icp", from: 1, description: "client_id \u2192 project_id", up: renameClientId },
|
|
307
|
+
{ kind: "signals", from: 1, description: "client_id \u2192 project_id", up: renameClientId },
|
|
308
|
+
{ kind: "campaign-config", from: 1, description: "client_id \u2192 project_id", up: renameClientId }
|
|
309
|
+
];
|
|
286
310
|
function migrateDocument(kind, doc, migrations = MIGRATIONS, target = FILE_VERSIONS[kind]) {
|
|
287
311
|
const raw = doc.get("version");
|
|
288
312
|
const from = typeof raw === "number" ? raw : Number(raw);
|
|
@@ -321,11 +345,11 @@ function copyContentHash(markdown) {
|
|
|
321
345
|
|
|
322
346
|
// src/commands/approve.ts
|
|
323
347
|
var USAGE = `Usage:
|
|
324
|
-
gtm approve icp --by "<Name (
|
|
325
|
-
gtm approve copy <campaign> --by "<Name (
|
|
348
|
+
gtm approve icp --by "<Name (role)>" [--notes "<what was approved>"] [--date YYYY-MM-DD]
|
|
349
|
+
gtm approve copy <campaign> --by "<Name (role)>" [--notes "<what was approved>"] [--date YYYY-MM-DD]`;
|
|
326
350
|
function load(file) {
|
|
327
351
|
if (!existsSync(file)) {
|
|
328
|
-
console.error(`${file} not found: run this from the root of a
|
|
352
|
+
console.error(`${file} not found: run this from the root of a project repo`);
|
|
329
353
|
return null;
|
|
330
354
|
}
|
|
331
355
|
const doc = parseDocument(readFileSync(file, "utf8"));
|
|
@@ -361,7 +385,7 @@ function runApprove(root2, args2, opts) {
|
|
|
361
385
|
}
|
|
362
386
|
const by = opts.by?.trim();
|
|
363
387
|
if (!by) {
|
|
364
|
-
console.error('--by is required: the human who approved, e.g. --by "Maria Chen (
|
|
388
|
+
console.error('--by is required: the human who approved, e.g. --by "Maria Chen (VP Marketing)"');
|
|
365
389
|
return false;
|
|
366
390
|
}
|
|
367
391
|
const date = opts.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
@@ -502,7 +526,7 @@ import { dirname as dirname3, join as join5 } from "path";
|
|
|
502
526
|
var SHIPPED_DIRS = ["skills", "templates", "providers", "schemas"];
|
|
503
527
|
function agentsMd() {
|
|
504
528
|
const header = `<!-- GENERATED by \`gtm sync\` from ${PACKAGE_NAME} v${GTM_VERSION}. Do not edit: it is overwritten on upgrade.
|
|
505
|
-
|
|
529
|
+
Project-specific rules go in PROJECT.md. Process changes go to the Veleads/gtm-core repo. -->
|
|
506
530
|
|
|
507
531
|
`;
|
|
508
532
|
return header + readFileSync4(join5(KIT_DIR, "AGENTS.md"), "utf8").replaceAll("{{GTM_VERSION}}", GTM_VERSION);
|
|
@@ -545,7 +569,7 @@ function runSync(root2, opts = {}) {
|
|
|
545
569
|
console.log(`in sync with ${PACKAGE_NAME} v${GTM_VERSION}`);
|
|
546
570
|
return true;
|
|
547
571
|
}
|
|
548
|
-
console.log(`out of sync with ${PACKAGE_NAME} v${GTM_VERSION} (run \`
|
|
572
|
+
console.log(`out of sync with ${PACKAGE_NAME} v${GTM_VERSION} (run \`npx gtm sync\`; never edit .gtm/ or AGENTS.md by hand):`);
|
|
549
573
|
for (const d of diffs) console.log(` ${d}`);
|
|
550
574
|
return false;
|
|
551
575
|
}
|
|
@@ -562,18 +586,18 @@ function runSync(root2, opts = {}) {
|
|
|
562
586
|
// src/commands/init.ts
|
|
563
587
|
function runInit(slug, opts) {
|
|
564
588
|
if (!slug || !Slug.safeParse(slug).success || !opts.name) {
|
|
565
|
-
console.error('Usage: gtm init <
|
|
589
|
+
console.error('Usage: gtm init <project-slug> --name "<Project Name>" [--dir <path>] (slug: lowercase, digits, hyphens)');
|
|
566
590
|
return false;
|
|
567
591
|
}
|
|
568
|
-
const root2 = resolve(opts.cwd, opts.dir ?? `gtm-
|
|
592
|
+
const root2 = resolve(opts.cwd, opts.dir ?? `gtm-project-${slug}`);
|
|
569
593
|
if (existsSync4(root2) && readdirSync2(root2).length > 0) {
|
|
570
594
|
console.error(`${root2} exists and is not empty`);
|
|
571
595
|
return false;
|
|
572
596
|
}
|
|
573
|
-
const vars = {
|
|
597
|
+
const vars = { PROJECT_ID: slug, PROJECT_NAME: opts.name, TODAY: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), GTM_VERSION };
|
|
574
598
|
const tpl = join6(KIT_DIR, "templates");
|
|
575
599
|
copyTemplate(join6(tpl, "repo"), root2, vars);
|
|
576
|
-
copyTemplate(join6(tpl, "
|
|
600
|
+
copyTemplate(join6(tpl, "project"), root2, vars, { rename: (rel) => rel === "project.yaml" ? rel : join6("context", rel) });
|
|
577
601
|
for (const d of ["campaigns", "tam"]) {
|
|
578
602
|
mkdirSync3(join6(root2, d), { recursive: true });
|
|
579
603
|
writeFileSync4(join6(root2, d, ".gitkeep"), "");
|
|
@@ -584,9 +608,9 @@ function runInit(slug, opts) {
|
|
|
584
608
|
Created ${root2} (core v${GTM_VERSION})${git.status === 0 ? " with git initialised" : ""}.
|
|
585
609
|
|
|
586
610
|
Next:
|
|
587
|
-
1. Create a PRIVATE GitHub repo for it (e.g. <your-org>/gtm-
|
|
588
|
-
|
|
589
|
-
2.
|
|
611
|
+
1. Create a PRIVATE GitHub repo for it (e.g. <your-org>/gtm-project-${slug}) and push this folder.
|
|
612
|
+
Project repos hold project context: never make them public.
|
|
613
|
+
2. npm install && npx gtm validate (or pnpm/yarn; TODO warnings remain until intake is done)
|
|
590
614
|
3. Open the repo in Claude Code or Codex and start with stage 00-intake.`);
|
|
591
615
|
return true;
|
|
592
616
|
}
|
|
@@ -602,22 +626,39 @@ Per AGENTS.md this is a mandatory stop: do not improvise the job with ad-hoc scr
|
|
|
602
626
|
}
|
|
603
627
|
|
|
604
628
|
// src/commands/migrate.ts
|
|
605
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
629
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, renameSync, writeFileSync as writeFileSync5 } from "fs";
|
|
606
630
|
import { join as join7, relative as relative2 } from "path";
|
|
607
631
|
import { parseDocument as parseDocument2 } from "yaml";
|
|
608
|
-
function
|
|
632
|
+
function projectYamlFiles(root2) {
|
|
609
633
|
const files = [
|
|
610
|
-
{ kind: "
|
|
634
|
+
{ kind: "project", file: join7(root2, "project.yaml") },
|
|
611
635
|
{ kind: "icp", file: join7(root2, "context", "icp.yaml") },
|
|
612
636
|
{ kind: "signals", file: join7(root2, "context", "signals.yaml") },
|
|
613
637
|
...subdirs(join7(root2, "campaigns")).map((c) => ({ kind: "campaign-config", file: join7(root2, "campaigns", c, "config.yaml") }))
|
|
614
638
|
];
|
|
615
639
|
return files.filter((f) => existsSync5(f.file));
|
|
616
640
|
}
|
|
641
|
+
function pendingRenames(root2) {
|
|
642
|
+
return FILE_RENAMES.filter((r) => existsSync5(join7(root2, r.from)) && !existsSync5(join7(root2, r.to)));
|
|
643
|
+
}
|
|
617
644
|
function runMigrate(root2, opts = {}) {
|
|
618
645
|
let ok = true;
|
|
619
646
|
let changed = 0;
|
|
620
|
-
for (const {
|
|
647
|
+
for (const { from, to } of pendingRenames(root2)) {
|
|
648
|
+
changed++;
|
|
649
|
+
console.log(` ${opts.dryRun ? "would rename" : "renamed"} ${from} \u2192 ${to}`);
|
|
650
|
+
if (opts.dryRun) continue;
|
|
651
|
+
renameSync(join7(root2, from), join7(root2, to));
|
|
652
|
+
if (to.endsWith(".md")) {
|
|
653
|
+
const p = join7(root2, to);
|
|
654
|
+
writeFileSync5(p, readFileSync5(p, "utf8").replace(new RegExp(`^# ${from.replace(".", "\\.")}`, "m"), `# ${to}`));
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (opts.dryRun && changed) {
|
|
658
|
+
console.log("(dry run: YAML migrations are listed after the renames are applied)");
|
|
659
|
+
return ok;
|
|
660
|
+
}
|
|
661
|
+
for (const { kind, file } of projectYamlFiles(root2)) {
|
|
621
662
|
const rel = relative2(root2, file);
|
|
622
663
|
const doc = parseDocument2(readFileSync5(file, "utf8"));
|
|
623
664
|
if (doc.errors.length) {
|
|
@@ -634,7 +675,7 @@ function runMigrate(root2, opts = {}) {
|
|
|
634
675
|
for (const a of res.applied) console.log(` ${opts.dryRun ? "would apply" : "applied"} ${rel}: ${a}`);
|
|
635
676
|
if (pinned) {
|
|
636
677
|
doc.setIn(["approval", "content_hash"], icpContentHash(doc.toJS()));
|
|
637
|
-
console.log(` re-pinned ICP approval hash in ${rel} (core migration, content approved by the
|
|
678
|
+
console.log(` re-pinned ICP approval hash in ${rel} (core migration, content approved by the project approver)`);
|
|
638
679
|
}
|
|
639
680
|
if (!opts.dryRun) writeFileSync5(file, doc.toString({ flowCollectionPadding: false, lineWidth: 0 }));
|
|
640
681
|
} catch (e) {
|
|
@@ -655,9 +696,9 @@ function runNewCampaign(root2, campaign) {
|
|
|
655
696
|
console.error("Usage: gtm new-campaign <campaign-slug>");
|
|
656
697
|
return false;
|
|
657
698
|
}
|
|
658
|
-
const
|
|
659
|
-
if (!existsSync6(
|
|
660
|
-
console.error("
|
|
699
|
+
const projectFile = join8(root2, "project.yaml");
|
|
700
|
+
if (!existsSync6(projectFile)) {
|
|
701
|
+
console.error("project.yaml not found: run this from the root of a project repo");
|
|
661
702
|
return false;
|
|
662
703
|
}
|
|
663
704
|
const dest = join8(root2, "campaigns", campaign);
|
|
@@ -665,8 +706,8 @@ function runNewCampaign(root2, campaign) {
|
|
|
665
706
|
console.error(`campaigns/${campaign} already exists`);
|
|
666
707
|
return false;
|
|
667
708
|
}
|
|
668
|
-
const
|
|
669
|
-
copyTemplate(join8(KIT_DIR, "templates", "campaign"), dest, {
|
|
709
|
+
const projectId = parse(readFileSync6(projectFile, "utf8"))?.project_id ?? "TODO";
|
|
710
|
+
copyTemplate(join8(KIT_DIR, "templates", "campaign"), dest, { PROJECT_ID: projectId, CAMPAIGN_ID: campaign, OWNER: process.env.GTM_USER ?? "TODO" });
|
|
670
711
|
mkdirSync4(join8(dest, "runs"), { recursive: true });
|
|
671
712
|
writeFileSync6(join8(dest, "runs", ".gitkeep"), "");
|
|
672
713
|
console.log(`Created campaigns/${campaign}. Branch: git switch -c campaign/${campaign}`);
|
|
@@ -688,9 +729,9 @@ function campaignStages(root2) {
|
|
|
688
729
|
return subdirs(join9(root2, "campaigns")).map((id) => ({ id, stage: readYaml(join9(root2, "campaigns", id, "config.yaml"))?.stage ?? "?" }));
|
|
689
730
|
}
|
|
690
731
|
function runStatus(root2) {
|
|
691
|
-
const
|
|
732
|
+
const project = readYaml(join9(root2, "project.yaml"));
|
|
692
733
|
const icp = readYaml(join9(root2, "context", "icp.yaml"));
|
|
693
|
-
console.log(`${
|
|
734
|
+
console.log(`${project?.name ?? "?"} (${project?.project_id ?? "?"}) \xB7 status ${project?.status ?? "?"} \xB7 core v${GTM_VERSION}`);
|
|
694
735
|
console.log(`ICP gate: ${icp?.approval?.status ?? "?"}${icp?.approval?.approved_by ? ` by ${icp.approval.approved_by} on ${icp.approval.approved_at}` : ""}
|
|
695
736
|
`);
|
|
696
737
|
const rows = subdirs(join9(root2, "campaigns")).map((id) => {
|
|
@@ -700,7 +741,7 @@ function runStatus(root2) {
|
|
|
700
741
|
return [id, cfg.stage ?? "?", cfg.gates?.copy?.status ?? "?", cfg.gates?.launch?.status ?? "?", cfg.owner ?? "?", last];
|
|
701
742
|
});
|
|
702
743
|
if (!rows.length) {
|
|
703
|
-
console.log("no campaigns yet:
|
|
744
|
+
console.log("no campaigns yet: npx gtm new-campaign <campaign-slug>");
|
|
704
745
|
return true;
|
|
705
746
|
}
|
|
706
747
|
const header = ["campaign", "stage", "copy gate", "launch gate", "owner", "latest run log"];
|
|
@@ -766,8 +807,8 @@ function loadVersioned(r, file, kind, schema) {
|
|
|
766
807
|
}
|
|
767
808
|
const expected = FILE_VERSIONS[kind];
|
|
768
809
|
if (raw?.version !== expected) {
|
|
769
|
-
if (typeof raw?.version === "number" && raw.version < expected) r.err(file, `is version ${raw.version}, core v${GTM_VERSION} expects ${expected}: run \`
|
|
770
|
-
else if (typeof raw?.version === "number") r.err(file, `is version ${raw.version}, newer than core v${GTM_VERSION} (${expected}): run \`
|
|
810
|
+
if (typeof raw?.version === "number" && raw.version < expected) r.err(file, `is version ${raw.version}, core v${GTM_VERSION} expects ${expected}: run \`npx gtm migrate\``);
|
|
811
|
+
else if (typeof raw?.version === "number") r.err(file, `is version ${raw.version}, newer than core v${GTM_VERSION} (${expected}): run \`npx gtm upgrade\``);
|
|
771
812
|
else r.err(file, `missing "version: ${expected}"`);
|
|
772
813
|
return null;
|
|
773
814
|
}
|
|
@@ -784,14 +825,14 @@ function checkCoreSync(r, root2) {
|
|
|
784
825
|
if (!/^\d+\.\d+\.\d+/.test(pinned)) r.err(join10(root2, "package.json"), `${PACKAGE_NAME} must be pinned exactly (got "${pinned}")`);
|
|
785
826
|
const versionFile = join10(root2, ".gtm", "VERSION");
|
|
786
827
|
const synced = existsSync8(versionFile) ? readFileSync8(versionFile, "utf8").trim() : null;
|
|
787
|
-
if (synced !== GTM_VERSION) r.err(versionFile, `is ${synced ?? "missing"}, installed core is ${GTM_VERSION}: run \`
|
|
788
|
-
if (!existsSync8(join10(root2, "AGENTS.md"))) r.err(join10(root2, "AGENTS.md"), "missing: run `
|
|
828
|
+
if (synced !== GTM_VERSION) r.err(versionFile, `is ${synced ?? "missing"}, installed core is ${GTM_VERSION}: run \`npx gtm sync\``);
|
|
829
|
+
if (!existsSync8(join10(root2, "AGENTS.md"))) r.err(join10(root2, "AGENTS.md"), "missing: run `npx gtm sync`");
|
|
789
830
|
}
|
|
790
831
|
function checkNoLeadData(r, root2) {
|
|
791
832
|
for (const rel of walk(root2, SKIP_DIRS)) {
|
|
792
833
|
const file = join10(root2, rel);
|
|
793
834
|
if (DATA_FILES.test(rel)) {
|
|
794
|
-
r.err(file, "data files are not allowed in a
|
|
835
|
+
r.err(file, "data files are not allowed in a project repo (lead data belongs in Postgres)");
|
|
795
836
|
continue;
|
|
796
837
|
}
|
|
797
838
|
if (!/\.(md|ya?ml|txt)$/i.test(rel) || rel === "pnpm-lock.yaml") continue;
|
|
@@ -811,14 +852,14 @@ function checkPinnedApproval(r, contentFile, approval, current, what, approveCmd
|
|
|
811
852
|
r.err(contentFile, `${what} changed after approval (approved ${pinned}, now ${current}). Re-present it to the approver and record it with \`${approveCmd} --by "<name>"\`, or revert the change`);
|
|
812
853
|
return false;
|
|
813
854
|
}
|
|
814
|
-
function validateCampaign(r, root2,
|
|
855
|
+
function validateCampaign(r, root2, projectId, campaign, icp, signals, icpApproved) {
|
|
815
856
|
const dir = join10(root2, "campaigns", campaign);
|
|
816
857
|
console.log(`campaign ${campaign}`);
|
|
817
858
|
for (const f of REQUIRED_CAMPAIGN) if (!existsSync8(join10(dir, f))) r.err(join10(dir, f), "missing");
|
|
818
859
|
const cfgFile = join10(dir, "config.yaml");
|
|
819
860
|
const cfg = loadVersioned(r, cfgFile, "campaign-config", CampaignConfig);
|
|
820
861
|
if (cfg) {
|
|
821
|
-
if (cfg.
|
|
862
|
+
if (cfg.project_id !== projectId) r.err(cfgFile, `project_id "${cfg.project_id}" does not match project.yaml "${projectId}"`);
|
|
822
863
|
if (cfg.campaign_id !== campaign) r.err(cfgFile, `campaign_id "${cfg.campaign_id}" does not match folder "${campaign}"`);
|
|
823
864
|
if (icp) {
|
|
824
865
|
const segs = new Set(icp.segments.map((s) => s.id));
|
|
@@ -841,7 +882,7 @@ function validateCampaign(r, root2, clientId, campaign, icp, signals, icpApprove
|
|
|
841
882
|
r.err(cfgFile, `stage is "${cfg.stage}" but the ICP gate (context/icp.yaml approval) is not approved, or the ICP changed since approval`);
|
|
842
883
|
}
|
|
843
884
|
const copyFile = join10(dir, "copy.md");
|
|
844
|
-
const copyApproved = existsSync8(copyFile) ? checkPinnedApproval(r, copyFile, cfg.gates.copy, copyContentHash(readFileSync8(copyFile, "utf8")), "copy", `
|
|
885
|
+
const copyApproved = existsSync8(copyFile) ? checkPinnedApproval(r, copyFile, cfg.gates.copy, copyContentHash(readFileSync8(copyFile, "utf8")), "copy", `npx gtm approve copy ${campaign}`) : false;
|
|
845
886
|
if (stageIndex(cfg.stage) > stageIndex("copy") && !copyApproved) {
|
|
846
887
|
r.err(cfgFile, `stage is "${cfg.stage}" but gates.copy is not approved, or copy.md changed since approval`);
|
|
847
888
|
}
|
|
@@ -866,22 +907,23 @@ function runValidate(root2, onlyCampaign) {
|
|
|
866
907
|
const r = new Report(root2);
|
|
867
908
|
console.log(`validating ${root2} against core v${GTM_VERSION}`);
|
|
868
909
|
checkCoreSync(r, root2);
|
|
869
|
-
const
|
|
870
|
-
const
|
|
910
|
+
for (const { from, to } of pendingRenames(root2)) r.err(join10(root2, from), `renamed to ${to} in core 0.3.0: run \`npx gtm migrate\``);
|
|
911
|
+
const meta = loadVersioned(r, join10(root2, "project.yaml"), "project", ProjectFile);
|
|
912
|
+
const projectId = meta?.project_id ?? "";
|
|
871
913
|
if (meta) {
|
|
872
914
|
for (const key of Object.keys(meta.features)) {
|
|
873
|
-
if (!FEATURES[key]) r.err(join10(root2, "
|
|
915
|
+
if (!FEATURES[key]) r.err(join10(root2, "project.yaml"), `unknown feature "${key}" (known: ${Object.keys(FEATURES).join(", ") || "none"})`);
|
|
874
916
|
}
|
|
875
917
|
}
|
|
876
|
-
if (!existsSync8(join10(root2, "
|
|
918
|
+
if (!existsSync8(join10(root2, "PROJECT.md"))) r.warn(join10(root2, "PROJECT.md"), "missing (project-specific rules for agents)");
|
|
877
919
|
for (const f of REQUIRED_CONTEXT) if (!existsSync8(join10(root2, "context", f))) r.err(join10(root2, "context", f), "missing");
|
|
878
920
|
const icpFile = join10(root2, "context", "icp.yaml");
|
|
879
921
|
const sigFile = join10(root2, "context", "signals.yaml");
|
|
880
922
|
const icp = existsSync8(icpFile) ? loadVersioned(r, icpFile, "icp", IcpFile) : null;
|
|
881
923
|
const signals = existsSync8(sigFile) ? loadVersioned(r, sigFile, "signals", SignalsFile) : null;
|
|
882
|
-
if (meta && icp && icp.
|
|
883
|
-
if (meta && signals && signals.
|
|
884
|
-
const icpApproved = icp ? checkPinnedApproval(r, icpFile, icp.approval, icpContentHash(parse3(readFileSync8(icpFile, "utf8"))), "ICP", "
|
|
924
|
+
if (meta && icp && icp.project_id !== projectId) r.err(icpFile, `project_id "${icp.project_id}" does not match project.yaml "${projectId}"`);
|
|
925
|
+
if (meta && signals && signals.project_id !== projectId) r.err(sigFile, `project_id "${signals.project_id}" does not match project.yaml "${projectId}"`);
|
|
926
|
+
const icpApproved = icp ? checkPinnedApproval(r, icpFile, icp.approval, icpContentHash(parse3(readFileSync8(icpFile, "utf8"))), "ICP", "npx gtm approve icp") : false;
|
|
885
927
|
checkNoLeadData(r, root2);
|
|
886
928
|
const campaigns = onlyCampaign ? [onlyCampaign] : subdirs(join10(root2, "campaigns"));
|
|
887
929
|
for (const c of campaigns) {
|
|
@@ -889,24 +931,31 @@ function runValidate(root2, onlyCampaign) {
|
|
|
889
931
|
r.err(join10(root2, "campaigns", c), "campaign folder not found");
|
|
890
932
|
continue;
|
|
891
933
|
}
|
|
892
|
-
validateCampaign(r, root2,
|
|
934
|
+
validateCampaign(r, root2, projectId, c, icp, signals, icpApproved);
|
|
893
935
|
}
|
|
894
936
|
r.summary();
|
|
895
937
|
return r.errors === 0;
|
|
896
938
|
}
|
|
897
939
|
|
|
898
940
|
// src/commands/upgrade.ts
|
|
899
|
-
function
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
941
|
+
function detectPackageManager(root2) {
|
|
942
|
+
if (existsSync9(join11(root2, "pnpm-lock.yaml"))) return "pnpm";
|
|
943
|
+
if (existsSync9(join11(root2, "yarn.lock"))) return "yarn";
|
|
944
|
+
return "npm";
|
|
945
|
+
}
|
|
946
|
+
function installExact(root2, pm, spec) {
|
|
947
|
+
const onPath = (bin) => spawnSync2(bin, ["--version"], { stdio: "ignore" }).status === 0;
|
|
948
|
+
const [cmd, args2] = pm === "npm" ? ["npm", ["install", "--save-dev", "--save-exact", spec]] : pm === "pnpm" ? onPath("pnpm") ? ["pnpm", ["add", "-D", "--save-exact", spec]] : ["npx", ["-y", "pnpm@10", "add", "-D", "--save-exact", spec]] : onPath("yarn") ? ["yarn", ["add", "-D", "--exact", spec]] : ["npx", ["-y", "yarn@1", "add", "-D", "--exact", spec]];
|
|
949
|
+
return spawnSync2(cmd, args2, { cwd: root2, stdio: "inherit" }).status ?? 1;
|
|
950
|
+
}
|
|
951
|
+
function gtm(root2, args2) {
|
|
952
|
+
return spawnSync2("npx", ["gtm", ...args2], { cwd: root2, stdio: "inherit" }).status ?? 1;
|
|
904
953
|
}
|
|
905
954
|
var FROZEN_STAGES = ["qa", "launch"];
|
|
906
955
|
function runUpgrade(root2, opts) {
|
|
907
956
|
const before = pinnedVersion(root2);
|
|
908
957
|
if (!before) {
|
|
909
|
-
console.error(`package.json does not depend on ${PACKAGE_NAME}: is this a
|
|
958
|
+
console.error(`package.json does not depend on ${PACKAGE_NAME}: is this a project repo?`);
|
|
910
959
|
return false;
|
|
911
960
|
}
|
|
912
961
|
const frozen = campaignStages(root2).filter((c) => FROZEN_STAGES.includes(c.stage));
|
|
@@ -921,11 +970,12 @@ function runUpgrade(root2, opts) {
|
|
|
921
970
|
return false;
|
|
922
971
|
}
|
|
923
972
|
const target = opts.version ?? "latest";
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
if (
|
|
927
|
-
if (
|
|
928
|
-
|
|
973
|
+
const pm = detectPackageManager(root2);
|
|
974
|
+
console.log(`Upgrading ${PACKAGE_NAME} ${before} \u2192 ${target} (with ${pm})`);
|
|
975
|
+
if (installExact(root2, pm, `${PACKAGE_NAME}@${target}`) !== 0) return false;
|
|
976
|
+
if (gtm(root2, ["sync"]) !== 0) return false;
|
|
977
|
+
if (gtm(root2, ["migrate"]) !== 0) return false;
|
|
978
|
+
const valid = gtm(root2, ["validate"]) === 0;
|
|
929
979
|
const after = pinnedVersion(root2) ?? target;
|
|
930
980
|
const changelog = join11(root2, "node_modules", ...PACKAGE_NAME.split("/"), "CHANGELOG.md");
|
|
931
981
|
if (existsSync9(changelog)) {
|
|
@@ -942,44 +992,44 @@ ${notes || "(no changelog entries)"}
|
|
|
942
992
|
// src/help.ts
|
|
943
993
|
var COMMANDS = {
|
|
944
994
|
init: {
|
|
945
|
-
usage: 'gtm init <
|
|
946
|
-
summary: "scaffold a new
|
|
995
|
+
usage: 'gtm init <project-slug> --name "<Project Name>" [--dir <path>]',
|
|
996
|
+
summary: "scaffold a new project repo",
|
|
947
997
|
details: [
|
|
948
|
-
"Creates a complete
|
|
998
|
+
"Creates a complete project repo: project.yaml, context/ templates, campaigns/, tam/, PROJECT.md,",
|
|
949
999
|
"CI workflows, .gtm/ (core kit) and AGENTS.md, pinned to this CLI's version, then runs git init.",
|
|
950
|
-
"Push the result to a PRIVATE repo: it will hold
|
|
1000
|
+
"Push the result to a PRIVATE repo: it will hold project context."
|
|
951
1001
|
],
|
|
952
|
-
writes: "A new folder (default ./gtm-
|
|
953
|
-
examples: ['npx @veleads/gtm init acme --name "Acme Corp"', 'npx @veleads/gtm init acme --name "Acme Corp" --dir ./
|
|
1002
|
+
writes: "A new folder (default ./gtm-project-<slug>). Refuses if it exists and is not empty.",
|
|
1003
|
+
examples: ['npx @veleads/gtm init acme --name "Acme Corp"', 'npx @veleads/gtm init acme --name "Acme Corp" --dir ./projects/acme']
|
|
954
1004
|
},
|
|
955
1005
|
"new-campaign": {
|
|
956
1006
|
usage: "gtm new-campaign <campaign-slug>",
|
|
957
1007
|
summary: "scaffold campaigns/<slug>/",
|
|
958
1008
|
details: ["Copies the campaign template (brief.md, config.yaml, copy.md, runs/). Owner comes from GTM_USER."],
|
|
959
1009
|
writes: "campaigns/<slug>/ (refuses if it exists).",
|
|
960
|
-
examples: ["git switch -c campaign/q1-uk-cfo &&
|
|
1010
|
+
examples: ["git switch -c campaign/q1-uk-cfo && npx gtm new-campaign q1-uk-cfo"]
|
|
961
1011
|
},
|
|
962
1012
|
validate: {
|
|
963
1013
|
usage: "gtm validate [campaign]",
|
|
964
1014
|
summary: "check repo against schemas, gates, data rules",
|
|
965
1015
|
details: [
|
|
966
|
-
"Checks
|
|
1016
|
+
"Checks project.yaml, context/ and campaigns against the schemas; segment/persona/signal references;",
|
|
967
1017
|
"gate order (no stage past an unapproved or changed ICP/copy approval); required files; run-log names;",
|
|
968
1018
|
"no lead data (blocks data files, warns on email addresses); .gtm/ matches the installed core.",
|
|
969
1019
|
"Exit code 1 if there is any error; warnings never fail."
|
|
970
1020
|
],
|
|
971
1021
|
writes: "Nothing (read-only).",
|
|
972
|
-
examples: ["
|
|
1022
|
+
examples: ["npx gtm validate", "npx gtm validate q1-uk-cfo"]
|
|
973
1023
|
},
|
|
974
1024
|
status: {
|
|
975
1025
|
usage: "gtm status",
|
|
976
1026
|
summary: "campaigns, stages, gates",
|
|
977
|
-
details: ["One screen:
|
|
1027
|
+
details: ["One screen: project, core version, ICP gate, and per campaign the stage, copy/launch gates, owner and latest run log."],
|
|
978
1028
|
writes: "Nothing (read-only).",
|
|
979
|
-
examples: ["
|
|
1029
|
+
examples: ["npx gtm status"]
|
|
980
1030
|
},
|
|
981
1031
|
approve: {
|
|
982
|
-
usage: 'gtm approve icp --by "<Name (
|
|
1032
|
+
usage: 'gtm approve icp --by "<Name (role)>" [--notes ".."] [--date YYYY-MM-DD]\ngtm approve copy <campaign> --by "<Name (role)>" [--notes ".."] [--date YYYY-MM-DD]',
|
|
983
1033
|
summary: "record a sign-off and pin a hash of the approved content",
|
|
984
1034
|
details: [
|
|
985
1035
|
"Run ONLY after the named human explicitly approved in this session (AGENTS.md section 4).",
|
|
@@ -988,7 +1038,7 @@ var COMMANDS = {
|
|
|
988
1038
|
"The launch gate is recorded by hand in config.yaml (its content is the lead set)."
|
|
989
1039
|
],
|
|
990
1040
|
writes: "context/icp.yaml \u2192 approval (icp), or campaigns/<campaign>/config.yaml \u2192 gates.copy (copy).",
|
|
991
|
-
examples: ['
|
|
1041
|
+
examples: ['npx gtm approve icp --by "Maria Chen (VP Marketing)" --notes "Segments A+B, personas CFO+AP"', 'npx gtm approve copy q1-uk-cfo --by "Maria Chen (VP Marketing)"']
|
|
992
1042
|
},
|
|
993
1043
|
sync: {
|
|
994
1044
|
usage: "gtm sync [--check]",
|
|
@@ -998,14 +1048,14 @@ var COMMANDS = {
|
|
|
998
1048
|
"--check only compares and exits 1 if they differ (CI uses this; never edit .gtm/ by hand)."
|
|
999
1049
|
],
|
|
1000
1050
|
writes: ".gtm/ (replaced entirely) and AGENTS.md; creates CLAUDE.md if missing. With --check: nothing.",
|
|
1001
|
-
examples: ["
|
|
1051
|
+
examples: ["npx gtm sync", "npx gtm sync --check"]
|
|
1002
1052
|
},
|
|
1003
1053
|
migrate: {
|
|
1004
1054
|
usage: "gtm migrate [--dry-run]",
|
|
1005
1055
|
summary: "upgrade YAML files to current schema versions",
|
|
1006
|
-
details: ["Applies core schema migrations to
|
|
1056
|
+
details: ["Applies core schema migrations to project.yaml, context/*.yaml and campaigns/*/config.yaml, keeping comments."],
|
|
1007
1057
|
writes: "Those YAML files, only if a migration applies. With --dry-run: nothing.",
|
|
1008
|
-
examples: ["
|
|
1058
|
+
examples: ["npx gtm migrate --dry-run", "npx gtm migrate"]
|
|
1009
1059
|
},
|
|
1010
1060
|
upgrade: {
|
|
1011
1061
|
usage: "gtm upgrade [version] [--force]",
|
|
@@ -1015,7 +1065,7 @@ var COMMANDS = {
|
|
|
1015
1065
|
"Refuses while a campaign is in qa or launch, or with uncommitted changes (--force overrides; ask a GTM engineer)."
|
|
1016
1066
|
],
|
|
1017
1067
|
writes: "package.json, lockfile, node_modules, .gtm/, AGENTS.md and migrated YAML files.",
|
|
1018
|
-
examples: ["
|
|
1068
|
+
examples: ["npx gtm upgrade", "npx gtm upgrade 0.3.0"]
|
|
1019
1069
|
},
|
|
1020
1070
|
job: {
|
|
1021
1071
|
usage: "gtm job <name> <campaign>",
|
|
@@ -1025,15 +1075,15 @@ var COMMANDS = {
|
|
|
1025
1075
|
"Not available yet: it exits 1. Per AGENTS.md this is a stop; do not improvise jobs with ad-hoc scripts."
|
|
1026
1076
|
],
|
|
1027
1077
|
writes: "Nothing yet.",
|
|
1028
|
-
examples: ["
|
|
1078
|
+
examples: ["npx gtm job tam q1-uk-cfo"]
|
|
1029
1079
|
}
|
|
1030
1080
|
};
|
|
1031
1081
|
function generalHelp(version) {
|
|
1032
1082
|
const width = Math.max(...Object.keys(COMMANDS).map((c) => c.length));
|
|
1033
1083
|
const lines = Object.entries(COMMANDS).map(([name, c]) => ` ${name.padEnd(width)} ${c.summary}`);
|
|
1034
|
-
return `gtm v${version}: CLI for gtm-brain
|
|
1084
|
+
return `gtm v${version}: CLI for gtm-brain project repos (@veleads/gtm)
|
|
1035
1085
|
|
|
1036
|
-
Usage: gtm <command> [options] (inside a
|
|
1086
|
+
Usage: gtm <command> [options] (inside a project repo: npx gtm <command>)
|
|
1037
1087
|
|
|
1038
1088
|
Commands:
|
|
1039
1089
|
${lines.join("\n")}
|
|
@@ -1047,7 +1097,7 @@ Global options:
|
|
|
1047
1097
|
|
|
1048
1098
|
Exit codes: 0 success, 1 failure or validation errors.
|
|
1049
1099
|
|
|
1050
|
-
For coding agents: inside a
|
|
1100
|
+
For coding agents: inside a project repo, read AGENTS.md first and follow it. Read-only commands
|
|
1051
1101
|
(validate, status, sync --check, migrate --dry-run) are always safe to run.`;
|
|
1052
1102
|
}
|
|
1053
1103
|
function commandHelp(name) {
|