@veleads/gtm 0.2.1 → 0.4.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 +19 -0
- package/README.md +19 -18
- package/dist/cli.js +251 -140
- package/kit/AGENTS.md +64 -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 +13 -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);
|
|
@@ -406,8 +430,8 @@ function runApprove(root2, args2, opts) {
|
|
|
406
430
|
|
|
407
431
|
// src/commands/init.ts
|
|
408
432
|
import { spawnSync } from "child_process";
|
|
409
|
-
import { existsSync as existsSync4,
|
|
410
|
-
import { join as join6, resolve } from "path";
|
|
433
|
+
import { appendFileSync, existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
434
|
+
import { basename, join as join6, relative as relative2, resolve } from "path";
|
|
411
435
|
|
|
412
436
|
// src/paths.ts
|
|
413
437
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
@@ -425,7 +449,7 @@ var KIT_DIR = kit;
|
|
|
425
449
|
var CHANGELOG_FILE = firstExisting([join2(pkgRoot, "CHANGELOG.md"), join2(pkgRoot, "..", "..", "CHANGELOG.md")]);
|
|
426
450
|
|
|
427
451
|
// src/template.ts
|
|
428
|
-
import {
|
|
452
|
+
import { mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
429
453
|
import { dirname as dirname2, join as join4 } from "path";
|
|
430
454
|
|
|
431
455
|
// src/fsutil.ts
|
|
@@ -481,19 +505,22 @@ function destName(rel) {
|
|
|
481
505
|
const [first, ...rest] = rel.split("/");
|
|
482
506
|
return [DOTFILES.has(first) ? `.${first}` : first, ...rest].join("/");
|
|
483
507
|
}
|
|
508
|
+
function planTemplate(src, vars, rename = destName) {
|
|
509
|
+
return walk(src).map((rel) => {
|
|
510
|
+
const raw = readFileSync3(join4(src, rel));
|
|
511
|
+
if (!/\.(md|ya?ml|json|txt|example)$|gitignore$/.test(rel)) return { rel: rename(rel), content: raw };
|
|
512
|
+
let text = raw.toString("utf8");
|
|
513
|
+
for (const [k, v] of Object.entries(vars)) text = text.replaceAll(`__${k}__`, v);
|
|
514
|
+
return { rel: rename(rel), content: text };
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
function writePlanned(root2, file) {
|
|
518
|
+
const out = join4(root2, file.rel);
|
|
519
|
+
mkdirSync(dirname2(out), { recursive: true });
|
|
520
|
+
writeFileSync2(out, file.content);
|
|
521
|
+
}
|
|
484
522
|
function copyTemplate(src, dest, vars, opts = {}) {
|
|
485
|
-
for (const
|
|
486
|
-
if (opts.only && !opts.only.includes(rel)) continue;
|
|
487
|
-
const out = join4(dest, (opts.rename ?? destName)(rel));
|
|
488
|
-
mkdirSync(dirname2(out), { recursive: true });
|
|
489
|
-
if (/\.(md|ya?ml|json|txt|example)$|gitignore$/.test(rel)) {
|
|
490
|
-
let text = readFileSync3(join4(src, rel), "utf8");
|
|
491
|
-
for (const [k, v] of Object.entries(vars)) text = text.replaceAll(`__${k}__`, v);
|
|
492
|
-
writeFileSync2(out, text);
|
|
493
|
-
} else {
|
|
494
|
-
cpSync(join4(src, rel), out);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
523
|
+
for (const file of planTemplate(src, vars, opts.rename)) writePlanned(dest, file);
|
|
497
524
|
}
|
|
498
525
|
|
|
499
526
|
// src/commands/sync.ts
|
|
@@ -502,7 +529,7 @@ import { dirname as dirname3, join as join5 } from "path";
|
|
|
502
529
|
var SHIPPED_DIRS = ["skills", "templates", "providers", "schemas"];
|
|
503
530
|
function agentsMd() {
|
|
504
531
|
const header = `<!-- GENERATED by \`gtm sync\` from ${PACKAGE_NAME} v${GTM_VERSION}. Do not edit: it is overwritten on upgrade.
|
|
505
|
-
|
|
532
|
+
Project-specific rules go in PROJECT.md. Process changes go to the Veleads/gtm-core repo. -->
|
|
506
533
|
|
|
507
534
|
`;
|
|
508
535
|
return header + readFileSync4(join5(KIT_DIR, "AGENTS.md"), "utf8").replaceAll("{{GTM_VERSION}}", GTM_VERSION);
|
|
@@ -545,7 +572,7 @@ function runSync(root2, opts = {}) {
|
|
|
545
572
|
console.log(`in sync with ${PACKAGE_NAME} v${GTM_VERSION}`);
|
|
546
573
|
return true;
|
|
547
574
|
}
|
|
548
|
-
console.log(`out of sync with ${PACKAGE_NAME} v${GTM_VERSION} (run \`
|
|
575
|
+
console.log(`out of sync with ${PACKAGE_NAME} v${GTM_VERSION} (run \`npx gtm sync\`; never edit .gtm/ or AGENTS.md by hand):`);
|
|
549
576
|
for (const d of diffs) console.log(` ${d}`);
|
|
550
577
|
return false;
|
|
551
578
|
}
|
|
@@ -560,33 +587,86 @@ function runSync(root2, opts = {}) {
|
|
|
560
587
|
}
|
|
561
588
|
|
|
562
589
|
// src/commands/init.ts
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
590
|
+
var USAGE2 = `Usage:
|
|
591
|
+
gtm init <project-slug> --name "<Project Name>" [--dir <path>] create ./gtm-project-<slug> (or <path>)
|
|
592
|
+
gtm init . --name "<Project Name>" [--slug <project-slug>] set up the current folder
|
|
593
|
+
Slugs: lowercase letters, digits and hyphens.`;
|
|
594
|
+
var MERGEABLE = /* @__PURE__ */ new Set([".gitignore", "README.md", "CLAUDE.md", "campaigns/.gitkeep", "tam/.gitkeep"]);
|
|
595
|
+
function slugFromFolder(path) {
|
|
596
|
+
return basename(path).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/^gtm-project-/, "");
|
|
597
|
+
}
|
|
598
|
+
function missingIgnoreLines(existing, ours) {
|
|
599
|
+
const have = new Set(existing.split("\n").map((l) => l.trim()));
|
|
600
|
+
return ours.split("\n").filter((l) => l.trim() && !l.trim().startsWith("#") && !have.has(l.trim()));
|
|
601
|
+
}
|
|
602
|
+
function runInit(target, opts) {
|
|
603
|
+
const here = target === "." || opts.dir === ".";
|
|
604
|
+
const root2 = resolve(opts.cwd, here ? opts.dir ?? "." : opts.dir ?? `gtm-project-${target ?? ""}`);
|
|
605
|
+
const slug = target && target !== "." ? target : opts.slug ?? slugFromFolder(root2);
|
|
606
|
+
if (!opts.name || !slug || !Slug.safeParse(slug).success) {
|
|
607
|
+
console.error(USAGE2);
|
|
608
|
+
if (!slug && here) console.error(`Couldn't derive a slug from the folder name "${basename(root2)}": pass one with --slug`);
|
|
609
|
+
else if (slug && !Slug.safeParse(slug).success) console.error(`"${slug}" is not a valid slug: pass one with --slug`);
|
|
566
610
|
return false;
|
|
567
611
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
console.error(`${root2} exists and is not empty`);
|
|
612
|
+
if (existsSync4(join6(root2, "project.yaml")) || existsSync4(join6(root2, "client.yaml"))) {
|
|
613
|
+
console.error(`${root2} is already a gtm project. Use \`npx gtm sync\` or \`npx gtm upgrade\` instead.`);
|
|
571
614
|
return false;
|
|
572
615
|
}
|
|
573
|
-
const vars = {
|
|
616
|
+
const vars = { PROJECT_ID: slug, PROJECT_NAME: opts.name, TODAY: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), GTM_VERSION };
|
|
574
617
|
const tpl = join6(KIT_DIR, "templates");
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
618
|
+
const planned = [
|
|
619
|
+
...planTemplate(join6(tpl, "repo"), vars),
|
|
620
|
+
...planTemplate(join6(tpl, "project"), vars, (rel) => rel === "project.yaml" ? rel : join6("context", rel)),
|
|
621
|
+
{ rel: "campaigns/.gitkeep", content: "" },
|
|
622
|
+
{ rel: "tam/.gitkeep", content: "" }
|
|
623
|
+
];
|
|
624
|
+
const coreOwned = [...expectedFiles().keys()];
|
|
625
|
+
const conflicts = [...planned.map((f) => f.rel), ...coreOwned].filter((rel) => existsSync4(join6(root2, rel)) && !MERGEABLE.has(rel));
|
|
626
|
+
if (existsSync4(join6(root2, ".gtm"))) conflicts.unshift(".gtm/");
|
|
627
|
+
if (conflicts.length) {
|
|
628
|
+
console.error(`Cannot set up ${root2}: these files already exist and belong to gtm. Nothing was written.`);
|
|
629
|
+
for (const c of [...new Set(conflicts)].slice(0, 15)) console.error(` \u2717 ${c}`);
|
|
630
|
+
console.error("Move them away (or use an empty folder) and run init again.");
|
|
631
|
+
return false;
|
|
580
632
|
}
|
|
633
|
+
const notes = [];
|
|
634
|
+
for (const file of planned) {
|
|
635
|
+
const path = join6(root2, file.rel);
|
|
636
|
+
if (!existsSync4(path)) {
|
|
637
|
+
writePlanned(root2, file);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (file.rel === ".gitignore") {
|
|
641
|
+
const add = missingIgnoreLines(readFileSync5(path, "utf8"), file.content.toString());
|
|
642
|
+
if (add.length) {
|
|
643
|
+
appendFileSync(path, `
|
|
644
|
+
# gtm-brain
|
|
645
|
+
${add.join("\n")}
|
|
646
|
+
`);
|
|
647
|
+
notes.push(`.gitignore: added ${add.length} gtm rule(s)`);
|
|
648
|
+
}
|
|
649
|
+
} else if (file.rel === "README.md") {
|
|
650
|
+
notes.push("README.md: kept yours (see .gtm/templates/repo/README.md for the gtm version)");
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const claude = join6(root2, "CLAUDE.md");
|
|
654
|
+
const hadClaude = existsSync4(claude);
|
|
581
655
|
runSync(root2);
|
|
582
|
-
|
|
656
|
+
if (hadClaude && !readFileSync5(claude, "utf8").includes("@AGENTS.md")) {
|
|
657
|
+
appendFileSync(claude, "\n@AGENTS.md\n");
|
|
658
|
+
notes.push("CLAUDE.md: added @AGENTS.md so Claude Code loads the operating manual");
|
|
659
|
+
}
|
|
660
|
+
const inRepo = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: root2, encoding: "utf8" }).stdout?.trim() === "true";
|
|
661
|
+
const gitNote = inRepo ? "existing git repository kept" : spawnSync("git", ["init", "-q", "-b", "main"], { cwd: root2 }).status === 0 ? "git initialised" : "git not initialised";
|
|
662
|
+
const where = relative2(opts.cwd, root2) || ".";
|
|
663
|
+
console.log(`
|
|
664
|
+
Set up project "${opts.name}" (${slug}) in ${root2} (core v${GTM_VERSION}; ${gitNote}).`);
|
|
665
|
+
for (const n of notes) console.log(` \xB7 ${n}`);
|
|
583
666
|
console.log(`
|
|
584
|
-
Created ${root2} (core v${GTM_VERSION})${git.status === 0 ? " with git initialised" : ""}.
|
|
585
|
-
|
|
586
667
|
Next:
|
|
587
|
-
1.
|
|
588
|
-
|
|
589
|
-
2. pnpm install && pnpm gtm validate (TODO warnings remain until intake is done)
|
|
668
|
+
1. ${where === "." ? "" : `cd ${where} && `}npm install && npx gtm validate (or pnpm/yarn; TODO warnings remain until intake is done)
|
|
669
|
+
2. ${inRepo ? "Commit and push to your PRIVATE repo." : `Create a PRIVATE GitHub repo (e.g. <your-org>/gtm-project-${slug}) and push.`} Project repos hold project context: never make them public.
|
|
590
670
|
3. Open the repo in Claude Code or Codex and start with stage 00-intake.`);
|
|
591
671
|
return true;
|
|
592
672
|
}
|
|
@@ -602,24 +682,41 @@ Per AGENTS.md this is a mandatory stop: do not improvise the job with ad-hoc scr
|
|
|
602
682
|
}
|
|
603
683
|
|
|
604
684
|
// src/commands/migrate.ts
|
|
605
|
-
import { existsSync as existsSync5, readFileSync as
|
|
606
|
-
import { join as join7, relative as
|
|
685
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, renameSync, writeFileSync as writeFileSync4 } from "fs";
|
|
686
|
+
import { join as join7, relative as relative3 } from "path";
|
|
607
687
|
import { parseDocument as parseDocument2 } from "yaml";
|
|
608
|
-
function
|
|
688
|
+
function projectYamlFiles(root2) {
|
|
609
689
|
const files = [
|
|
610
|
-
{ kind: "
|
|
690
|
+
{ kind: "project", file: join7(root2, "project.yaml") },
|
|
611
691
|
{ kind: "icp", file: join7(root2, "context", "icp.yaml") },
|
|
612
692
|
{ kind: "signals", file: join7(root2, "context", "signals.yaml") },
|
|
613
693
|
...subdirs(join7(root2, "campaigns")).map((c) => ({ kind: "campaign-config", file: join7(root2, "campaigns", c, "config.yaml") }))
|
|
614
694
|
];
|
|
615
695
|
return files.filter((f) => existsSync5(f.file));
|
|
616
696
|
}
|
|
697
|
+
function pendingRenames(root2) {
|
|
698
|
+
return FILE_RENAMES.filter((r) => existsSync5(join7(root2, r.from)) && !existsSync5(join7(root2, r.to)));
|
|
699
|
+
}
|
|
617
700
|
function runMigrate(root2, opts = {}) {
|
|
618
701
|
let ok = true;
|
|
619
702
|
let changed = 0;
|
|
620
|
-
for (const {
|
|
621
|
-
|
|
622
|
-
|
|
703
|
+
for (const { from, to } of pendingRenames(root2)) {
|
|
704
|
+
changed++;
|
|
705
|
+
console.log(` ${opts.dryRun ? "would rename" : "renamed"} ${from} \u2192 ${to}`);
|
|
706
|
+
if (opts.dryRun) continue;
|
|
707
|
+
renameSync(join7(root2, from), join7(root2, to));
|
|
708
|
+
if (to.endsWith(".md")) {
|
|
709
|
+
const p = join7(root2, to);
|
|
710
|
+
writeFileSync4(p, readFileSync6(p, "utf8").replace(new RegExp(`^# ${from.replace(".", "\\.")}`, "m"), `# ${to}`));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (opts.dryRun && changed) {
|
|
714
|
+
console.log("(dry run: YAML migrations are listed after the renames are applied)");
|
|
715
|
+
return ok;
|
|
716
|
+
}
|
|
717
|
+
for (const { kind, file } of projectYamlFiles(root2)) {
|
|
718
|
+
const rel = relative3(root2, file);
|
|
719
|
+
const doc = parseDocument2(readFileSync6(file, "utf8"));
|
|
623
720
|
if (doc.errors.length) {
|
|
624
721
|
console.log(` \u2717 ${rel}: invalid YAML: ${doc.errors[0].message}`);
|
|
625
722
|
ok = false;
|
|
@@ -634,9 +731,9 @@ function runMigrate(root2, opts = {}) {
|
|
|
634
731
|
for (const a of res.applied) console.log(` ${opts.dryRun ? "would apply" : "applied"} ${rel}: ${a}`);
|
|
635
732
|
if (pinned) {
|
|
636
733
|
doc.setIn(["approval", "content_hash"], icpContentHash(doc.toJS()));
|
|
637
|
-
console.log(` re-pinned ICP approval hash in ${rel} (core migration, content approved by the
|
|
734
|
+
console.log(` re-pinned ICP approval hash in ${rel} (core migration, content approved by the project approver)`);
|
|
638
735
|
}
|
|
639
|
-
if (!opts.dryRun)
|
|
736
|
+
if (!opts.dryRun) writeFileSync4(file, doc.toString({ flowCollectionPadding: false, lineWidth: 0 }));
|
|
640
737
|
} catch (e) {
|
|
641
738
|
console.log(` \u2717 ${rel}: ${e.message}`);
|
|
642
739
|
ok = false;
|
|
@@ -647,7 +744,7 @@ function runMigrate(root2, opts = {}) {
|
|
|
647
744
|
}
|
|
648
745
|
|
|
649
746
|
// src/commands/new-campaign.ts
|
|
650
|
-
import { existsSync as existsSync6, mkdirSync as
|
|
747
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
651
748
|
import { join as join8 } from "path";
|
|
652
749
|
import { parse } from "yaml";
|
|
653
750
|
function runNewCampaign(root2, campaign) {
|
|
@@ -655,9 +752,9 @@ function runNewCampaign(root2, campaign) {
|
|
|
655
752
|
console.error("Usage: gtm new-campaign <campaign-slug>");
|
|
656
753
|
return false;
|
|
657
754
|
}
|
|
658
|
-
const
|
|
659
|
-
if (!existsSync6(
|
|
660
|
-
console.error("
|
|
755
|
+
const projectFile = join8(root2, "project.yaml");
|
|
756
|
+
if (!existsSync6(projectFile)) {
|
|
757
|
+
console.error("project.yaml not found: run this from the root of a project repo");
|
|
661
758
|
return false;
|
|
662
759
|
}
|
|
663
760
|
const dest = join8(root2, "campaigns", campaign);
|
|
@@ -665,21 +762,21 @@ function runNewCampaign(root2, campaign) {
|
|
|
665
762
|
console.error(`campaigns/${campaign} already exists`);
|
|
666
763
|
return false;
|
|
667
764
|
}
|
|
668
|
-
const
|
|
669
|
-
copyTemplate(join8(KIT_DIR, "templates", "campaign"), dest, {
|
|
670
|
-
|
|
671
|
-
|
|
765
|
+
const projectId = parse(readFileSync7(projectFile, "utf8"))?.project_id ?? "TODO";
|
|
766
|
+
copyTemplate(join8(KIT_DIR, "templates", "campaign"), dest, { PROJECT_ID: projectId, CAMPAIGN_ID: campaign, OWNER: process.env.GTM_USER ?? "TODO" });
|
|
767
|
+
mkdirSync3(join8(dest, "runs"), { recursive: true });
|
|
768
|
+
writeFileSync5(join8(dest, "runs", ".gitkeep"), "");
|
|
672
769
|
console.log(`Created campaigns/${campaign}. Branch: git switch -c campaign/${campaign}`);
|
|
673
770
|
return true;
|
|
674
771
|
}
|
|
675
772
|
|
|
676
773
|
// src/commands/status.ts
|
|
677
|
-
import { existsSync as existsSync7, readdirSync as
|
|
774
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync8 } from "fs";
|
|
678
775
|
import { join as join9 } from "path";
|
|
679
776
|
import { parse as parse2 } from "yaml";
|
|
680
777
|
function readYaml(file) {
|
|
681
778
|
try {
|
|
682
|
-
return parse2(
|
|
779
|
+
return parse2(readFileSync8(file, "utf8"));
|
|
683
780
|
} catch {
|
|
684
781
|
return null;
|
|
685
782
|
}
|
|
@@ -688,19 +785,19 @@ function campaignStages(root2) {
|
|
|
688
785
|
return subdirs(join9(root2, "campaigns")).map((id) => ({ id, stage: readYaml(join9(root2, "campaigns", id, "config.yaml"))?.stage ?? "?" }));
|
|
689
786
|
}
|
|
690
787
|
function runStatus(root2) {
|
|
691
|
-
const
|
|
788
|
+
const project = readYaml(join9(root2, "project.yaml"));
|
|
692
789
|
const icp = readYaml(join9(root2, "context", "icp.yaml"));
|
|
693
|
-
console.log(`${
|
|
790
|
+
console.log(`${project?.name ?? "?"} (${project?.project_id ?? "?"}) \xB7 status ${project?.status ?? "?"} \xB7 core v${GTM_VERSION}`);
|
|
694
791
|
console.log(`ICP gate: ${icp?.approval?.status ?? "?"}${icp?.approval?.approved_by ? ` by ${icp.approval.approved_by} on ${icp.approval.approved_at}` : ""}
|
|
695
792
|
`);
|
|
696
793
|
const rows = subdirs(join9(root2, "campaigns")).map((id) => {
|
|
697
794
|
const cfg = readYaml(join9(root2, "campaigns", id, "config.yaml")) ?? {};
|
|
698
795
|
const runsDir = join9(root2, "campaigns", id, "runs");
|
|
699
|
-
const last = existsSync7(runsDir) ?
|
|
796
|
+
const last = existsSync7(runsDir) ? readdirSync2(runsDir).filter((f) => f.endsWith(".md")).sort().at(-1) ?? "-" : "-";
|
|
700
797
|
return [id, cfg.stage ?? "?", cfg.gates?.copy?.status ?? "?", cfg.gates?.launch?.status ?? "?", cfg.owner ?? "?", last];
|
|
701
798
|
});
|
|
702
799
|
if (!rows.length) {
|
|
703
|
-
console.log("no campaigns yet:
|
|
800
|
+
console.log("no campaigns yet: npx gtm new-campaign <campaign-slug>");
|
|
704
801
|
return true;
|
|
705
802
|
}
|
|
706
803
|
const header = ["campaign", "stage", "copy gate", "launch gate", "owner", "latest run log"];
|
|
@@ -714,7 +811,7 @@ function runStatus(root2) {
|
|
|
714
811
|
|
|
715
812
|
// src/commands/upgrade.ts
|
|
716
813
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
717
|
-
import { existsSync as existsSync9, readFileSync as
|
|
814
|
+
import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
|
|
718
815
|
import { join as join11 } from "path";
|
|
719
816
|
|
|
720
817
|
// src/changelog.ts
|
|
@@ -733,7 +830,7 @@ function changelogBetween(text, from, to) {
|
|
|
733
830
|
}
|
|
734
831
|
|
|
735
832
|
// src/commands/validate.ts
|
|
736
|
-
import { existsSync as existsSync8, readdirSync as
|
|
833
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
|
|
737
834
|
import { join as join10 } from "path";
|
|
738
835
|
import { parse as parse3 } from "yaml";
|
|
739
836
|
var REQUIRED_CONTEXT = ["intake.md", "icp.md", "icp.yaml", "signals.yaml", "personas.md", "offer.md", "proof.md", "voice.md"];
|
|
@@ -746,7 +843,7 @@ var SKIP_DIRS = ["node_modules", ".git", ".gtm"];
|
|
|
746
843
|
var stageIndex = (s) => Stage.options.indexOf(s);
|
|
747
844
|
function readPackageJson(root2) {
|
|
748
845
|
const p = join10(root2, "package.json");
|
|
749
|
-
return existsSync8(p) ? JSON.parse(
|
|
846
|
+
return existsSync8(p) ? JSON.parse(readFileSync9(p, "utf8")) : null;
|
|
750
847
|
}
|
|
751
848
|
function pinnedVersion(root2) {
|
|
752
849
|
const pkg = readPackageJson(root2);
|
|
@@ -759,15 +856,15 @@ function loadVersioned(r, file, kind, schema) {
|
|
|
759
856
|
}
|
|
760
857
|
let raw;
|
|
761
858
|
try {
|
|
762
|
-
raw = parse3(
|
|
859
|
+
raw = parse3(readFileSync9(file, "utf8"));
|
|
763
860
|
} catch (e) {
|
|
764
861
|
r.err(file, `invalid YAML: ${e.message}`);
|
|
765
862
|
return null;
|
|
766
863
|
}
|
|
767
864
|
const expected = FILE_VERSIONS[kind];
|
|
768
865
|
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 \`
|
|
866
|
+
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\``);
|
|
867
|
+
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
868
|
else r.err(file, `missing "version: ${expected}"`);
|
|
772
869
|
return null;
|
|
773
870
|
}
|
|
@@ -783,20 +880,20 @@ function checkCoreSync(r, root2) {
|
|
|
783
880
|
if (!pinned) return;
|
|
784
881
|
if (!/^\d+\.\d+\.\d+/.test(pinned)) r.err(join10(root2, "package.json"), `${PACKAGE_NAME} must be pinned exactly (got "${pinned}")`);
|
|
785
882
|
const versionFile = join10(root2, ".gtm", "VERSION");
|
|
786
|
-
const synced = existsSync8(versionFile) ?
|
|
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 `
|
|
883
|
+
const synced = existsSync8(versionFile) ? readFileSync9(versionFile, "utf8").trim() : null;
|
|
884
|
+
if (synced !== GTM_VERSION) r.err(versionFile, `is ${synced ?? "missing"}, installed core is ${GTM_VERSION}: run \`npx gtm sync\``);
|
|
885
|
+
if (!existsSync8(join10(root2, "AGENTS.md"))) r.err(join10(root2, "AGENTS.md"), "missing: run `npx gtm sync`");
|
|
789
886
|
}
|
|
790
887
|
function checkNoLeadData(r, root2) {
|
|
791
888
|
for (const rel of walk(root2, SKIP_DIRS)) {
|
|
792
889
|
const file = join10(root2, rel);
|
|
793
890
|
if (DATA_FILES.test(rel)) {
|
|
794
|
-
r.err(file, "data files are not allowed in a
|
|
891
|
+
r.err(file, "data files are not allowed in a project repo (lead data belongs in Postgres)");
|
|
795
892
|
continue;
|
|
796
893
|
}
|
|
797
894
|
if (!/\.(md|ya?ml|txt)$/i.test(rel) || rel === "pnpm-lock.yaml") continue;
|
|
798
|
-
if (/\.ya?ml$/i.test(rel) && /\bTODO\b/.test(
|
|
799
|
-
const found = (
|
|
895
|
+
if (/\.ya?ml$/i.test(rel) && /\bTODO\b/.test(readFileSync9(file, "utf8"))) r.warn(file, "still contains TODO placeholders");
|
|
896
|
+
const found = (readFileSync9(file, "utf8").match(EMAIL) ?? []).filter((e) => !ALLOWED_EMAIL.test(e));
|
|
800
897
|
if (found.length) r.warn(file, `contains email address(es) ${[...new Set(found)].slice(0, 3).join(", ")}: confirm none are prospects`);
|
|
801
898
|
}
|
|
802
899
|
}
|
|
@@ -811,14 +908,14 @@ function checkPinnedApproval(r, contentFile, approval, current, what, approveCmd
|
|
|
811
908
|
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
909
|
return false;
|
|
813
910
|
}
|
|
814
|
-
function validateCampaign(r, root2,
|
|
911
|
+
function validateCampaign(r, root2, projectId, campaign, icp, signals, icpApproved) {
|
|
815
912
|
const dir = join10(root2, "campaigns", campaign);
|
|
816
913
|
console.log(`campaign ${campaign}`);
|
|
817
914
|
for (const f of REQUIRED_CAMPAIGN) if (!existsSync8(join10(dir, f))) r.err(join10(dir, f), "missing");
|
|
818
915
|
const cfgFile = join10(dir, "config.yaml");
|
|
819
916
|
const cfg = loadVersioned(r, cfgFile, "campaign-config", CampaignConfig);
|
|
820
917
|
if (cfg) {
|
|
821
|
-
if (cfg.
|
|
918
|
+
if (cfg.project_id !== projectId) r.err(cfgFile, `project_id "${cfg.project_id}" does not match project.yaml "${projectId}"`);
|
|
822
919
|
if (cfg.campaign_id !== campaign) r.err(cfgFile, `campaign_id "${cfg.campaign_id}" does not match folder "${campaign}"`);
|
|
823
920
|
if (icp) {
|
|
824
921
|
const segs = new Set(icp.segments.map((s) => s.id));
|
|
@@ -841,7 +938,7 @@ function validateCampaign(r, root2, clientId, campaign, icp, signals, icpApprove
|
|
|
841
938
|
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
939
|
}
|
|
843
940
|
const copyFile = join10(dir, "copy.md");
|
|
844
|
-
const copyApproved = existsSync8(copyFile) ? checkPinnedApproval(r, copyFile, cfg.gates.copy, copyContentHash(
|
|
941
|
+
const copyApproved = existsSync8(copyFile) ? checkPinnedApproval(r, copyFile, cfg.gates.copy, copyContentHash(readFileSync9(copyFile, "utf8")), "copy", `npx gtm approve copy ${campaign}`) : false;
|
|
845
942
|
if (stageIndex(cfg.stage) > stageIndex("copy") && !copyApproved) {
|
|
846
943
|
r.err(cfgFile, `stage is "${cfg.stage}" but gates.copy is not approved, or copy.md changed since approval`);
|
|
847
944
|
}
|
|
@@ -857,7 +954,7 @@ function validateCampaign(r, root2, clientId, campaign, icp, signals, icpApprove
|
|
|
857
954
|
}
|
|
858
955
|
const runs = join10(dir, "runs");
|
|
859
956
|
if (existsSync8(runs)) {
|
|
860
|
-
for (const f of
|
|
957
|
+
for (const f of readdirSync3(runs).filter((f2) => !f2.startsWith("."))) {
|
|
861
958
|
if (!RUN_LOG_NAME.test(f)) r.err(join10(runs, f), "run logs must be named YYYY-MM-DD-<user>[-n].md");
|
|
862
959
|
}
|
|
863
960
|
}
|
|
@@ -866,22 +963,23 @@ function runValidate(root2, onlyCampaign) {
|
|
|
866
963
|
const r = new Report(root2);
|
|
867
964
|
console.log(`validating ${root2} against core v${GTM_VERSION}`);
|
|
868
965
|
checkCoreSync(r, root2);
|
|
869
|
-
const
|
|
870
|
-
const
|
|
966
|
+
for (const { from, to } of pendingRenames(root2)) r.err(join10(root2, from), `renamed to ${to} in core 0.3.0: run \`npx gtm migrate\``);
|
|
967
|
+
const meta = loadVersioned(r, join10(root2, "project.yaml"), "project", ProjectFile);
|
|
968
|
+
const projectId = meta?.project_id ?? "";
|
|
871
969
|
if (meta) {
|
|
872
970
|
for (const key of Object.keys(meta.features)) {
|
|
873
|
-
if (!FEATURES[key]) r.err(join10(root2, "
|
|
971
|
+
if (!FEATURES[key]) r.err(join10(root2, "project.yaml"), `unknown feature "${key}" (known: ${Object.keys(FEATURES).join(", ") || "none"})`);
|
|
874
972
|
}
|
|
875
973
|
}
|
|
876
|
-
if (!existsSync8(join10(root2, "
|
|
974
|
+
if (!existsSync8(join10(root2, "PROJECT.md"))) r.warn(join10(root2, "PROJECT.md"), "missing (project-specific rules for agents)");
|
|
877
975
|
for (const f of REQUIRED_CONTEXT) if (!existsSync8(join10(root2, "context", f))) r.err(join10(root2, "context", f), "missing");
|
|
878
976
|
const icpFile = join10(root2, "context", "icp.yaml");
|
|
879
977
|
const sigFile = join10(root2, "context", "signals.yaml");
|
|
880
978
|
const icp = existsSync8(icpFile) ? loadVersioned(r, icpFile, "icp", IcpFile) : null;
|
|
881
979
|
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(
|
|
980
|
+
if (meta && icp && icp.project_id !== projectId) r.err(icpFile, `project_id "${icp.project_id}" does not match project.yaml "${projectId}"`);
|
|
981
|
+
if (meta && signals && signals.project_id !== projectId) r.err(sigFile, `project_id "${signals.project_id}" does not match project.yaml "${projectId}"`);
|
|
982
|
+
const icpApproved = icp ? checkPinnedApproval(r, icpFile, icp.approval, icpContentHash(parse3(readFileSync9(icpFile, "utf8"))), "ICP", "npx gtm approve icp") : false;
|
|
885
983
|
checkNoLeadData(r, root2);
|
|
886
984
|
const campaigns = onlyCampaign ? [onlyCampaign] : subdirs(join10(root2, "campaigns"));
|
|
887
985
|
for (const c of campaigns) {
|
|
@@ -889,24 +987,31 @@ function runValidate(root2, onlyCampaign) {
|
|
|
889
987
|
r.err(join10(root2, "campaigns", c), "campaign folder not found");
|
|
890
988
|
continue;
|
|
891
989
|
}
|
|
892
|
-
validateCampaign(r, root2,
|
|
990
|
+
validateCampaign(r, root2, projectId, c, icp, signals, icpApproved);
|
|
893
991
|
}
|
|
894
992
|
r.summary();
|
|
895
993
|
return r.errors === 0;
|
|
896
994
|
}
|
|
897
995
|
|
|
898
996
|
// src/commands/upgrade.ts
|
|
899
|
-
function
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
997
|
+
function detectPackageManager(root2) {
|
|
998
|
+
if (existsSync9(join11(root2, "pnpm-lock.yaml"))) return "pnpm";
|
|
999
|
+
if (existsSync9(join11(root2, "yarn.lock"))) return "yarn";
|
|
1000
|
+
return "npm";
|
|
1001
|
+
}
|
|
1002
|
+
function installExact(root2, pm, spec) {
|
|
1003
|
+
const onPath = (bin) => spawnSync2(bin, ["--version"], { stdio: "ignore" }).status === 0;
|
|
1004
|
+
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]];
|
|
1005
|
+
return spawnSync2(cmd, args2, { cwd: root2, stdio: "inherit" }).status ?? 1;
|
|
1006
|
+
}
|
|
1007
|
+
function gtm(root2, args2) {
|
|
1008
|
+
return spawnSync2("npx", ["gtm", ...args2], { cwd: root2, stdio: "inherit" }).status ?? 1;
|
|
904
1009
|
}
|
|
905
1010
|
var FROZEN_STAGES = ["qa", "launch"];
|
|
906
1011
|
function runUpgrade(root2, opts) {
|
|
907
1012
|
const before = pinnedVersion(root2);
|
|
908
1013
|
if (!before) {
|
|
909
|
-
console.error(`package.json does not depend on ${PACKAGE_NAME}: is this a
|
|
1014
|
+
console.error(`package.json does not depend on ${PACKAGE_NAME}: is this a project repo?`);
|
|
910
1015
|
return false;
|
|
911
1016
|
}
|
|
912
1017
|
const frozen = campaignStages(root2).filter((c) => FROZEN_STAGES.includes(c.stage));
|
|
@@ -921,15 +1026,16 @@ function runUpgrade(root2, opts) {
|
|
|
921
1026
|
return false;
|
|
922
1027
|
}
|
|
923
1028
|
const target = opts.version ?? "latest";
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
if (
|
|
927
|
-
if (
|
|
928
|
-
|
|
1029
|
+
const pm = detectPackageManager(root2);
|
|
1030
|
+
console.log(`Upgrading ${PACKAGE_NAME} ${before} \u2192 ${target} (with ${pm})`);
|
|
1031
|
+
if (installExact(root2, pm, `${PACKAGE_NAME}@${target}`) !== 0) return false;
|
|
1032
|
+
if (gtm(root2, ["sync"]) !== 0) return false;
|
|
1033
|
+
if (gtm(root2, ["migrate"]) !== 0) return false;
|
|
1034
|
+
const valid = gtm(root2, ["validate"]) === 0;
|
|
929
1035
|
const after = pinnedVersion(root2) ?? target;
|
|
930
1036
|
const changelog = join11(root2, "node_modules", ...PACKAGE_NAME.split("/"), "CHANGELOG.md");
|
|
931
1037
|
if (existsSync9(changelog)) {
|
|
932
|
-
const notes = changelogBetween(
|
|
1038
|
+
const notes = changelogBetween(readFileSync10(changelog, "utf8"), before, after);
|
|
933
1039
|
console.log(`
|
|
934
1040
|
=== Changes ${before} \u2192 ${after} ===
|
|
935
1041
|
${notes || "(no changelog entries)"}
|
|
@@ -942,44 +1048,48 @@ ${notes || "(no changelog entries)"}
|
|
|
942
1048
|
// src/help.ts
|
|
943
1049
|
var COMMANDS = {
|
|
944
1050
|
init: {
|
|
945
|
-
usage: 'gtm init <
|
|
946
|
-
summary:
|
|
1051
|
+
usage: 'gtm init <project-slug> --name "<Project Name>" [--dir <path>]\ngtm init . --name "<Project Name>" [--slug <project-slug>]',
|
|
1052
|
+
summary: 'scaffold a project repo (new folder, or the current one with ".")',
|
|
947
1053
|
details: [
|
|
948
|
-
"Creates a complete
|
|
949
|
-
"CI workflows, .gtm/ (core kit) and AGENTS.md, pinned to this CLI's version
|
|
950
|
-
"
|
|
1054
|
+
"Creates a complete project repo: project.yaml, context/ templates, campaigns/, tam/, PROJECT.md,",
|
|
1055
|
+
"CI workflows, .gtm/ (core kit) and AGENTS.md, pinned to this CLI's version.",
|
|
1056
|
+
'"." sets up the current folder; the slug comes from the folder name (gtm-project-acme \u2192 acme) or --slug.',
|
|
1057
|
+
"Works in a fresh clone: .git is kept (no git init inside an existing repo), README.md is kept,",
|
|
1058
|
+
".gitignore gets gtm rules appended, CLAUDE.md gets @AGENTS.md. Any other existing gtm file",
|
|
1059
|
+
"(e.g. AGENTS.md, .gtm/, package.json) stops init before anything is written.",
|
|
1060
|
+
"Push the result to a PRIVATE repo: it will hold project context."
|
|
951
1061
|
],
|
|
952
|
-
writes:
|
|
953
|
-
examples: ['npx @veleads/gtm init
|
|
1062
|
+
writes: 'The target folder (new ./gtm-project-<slug>, --dir, or the current folder with "."). Never overwrites existing files.',
|
|
1063
|
+
examples: ['npx @veleads/gtm init . --name "Acme Corp"', 'npx @veleads/gtm init acme --name "Acme Corp"', 'npx @veleads/gtm init . --name "Acme Corp" --slug acme']
|
|
954
1064
|
},
|
|
955
1065
|
"new-campaign": {
|
|
956
1066
|
usage: "gtm new-campaign <campaign-slug>",
|
|
957
1067
|
summary: "scaffold campaigns/<slug>/",
|
|
958
1068
|
details: ["Copies the campaign template (brief.md, config.yaml, copy.md, runs/). Owner comes from GTM_USER."],
|
|
959
1069
|
writes: "campaigns/<slug>/ (refuses if it exists).",
|
|
960
|
-
examples: ["git switch -c campaign/q1-uk-cfo &&
|
|
1070
|
+
examples: ["git switch -c campaign/q1-uk-cfo && npx gtm new-campaign q1-uk-cfo"]
|
|
961
1071
|
},
|
|
962
1072
|
validate: {
|
|
963
1073
|
usage: "gtm validate [campaign]",
|
|
964
1074
|
summary: "check repo against schemas, gates, data rules",
|
|
965
1075
|
details: [
|
|
966
|
-
"Checks
|
|
1076
|
+
"Checks project.yaml, context/ and campaigns against the schemas; segment/persona/signal references;",
|
|
967
1077
|
"gate order (no stage past an unapproved or changed ICP/copy approval); required files; run-log names;",
|
|
968
1078
|
"no lead data (blocks data files, warns on email addresses); .gtm/ matches the installed core.",
|
|
969
1079
|
"Exit code 1 if there is any error; warnings never fail."
|
|
970
1080
|
],
|
|
971
1081
|
writes: "Nothing (read-only).",
|
|
972
|
-
examples: ["
|
|
1082
|
+
examples: ["npx gtm validate", "npx gtm validate q1-uk-cfo"]
|
|
973
1083
|
},
|
|
974
1084
|
status: {
|
|
975
1085
|
usage: "gtm status",
|
|
976
1086
|
summary: "campaigns, stages, gates",
|
|
977
|
-
details: ["One screen:
|
|
1087
|
+
details: ["One screen: project, core version, ICP gate, and per campaign the stage, copy/launch gates, owner and latest run log."],
|
|
978
1088
|
writes: "Nothing (read-only).",
|
|
979
|
-
examples: ["
|
|
1089
|
+
examples: ["npx gtm status"]
|
|
980
1090
|
},
|
|
981
1091
|
approve: {
|
|
982
|
-
usage: 'gtm approve icp --by "<Name (
|
|
1092
|
+
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
1093
|
summary: "record a sign-off and pin a hash of the approved content",
|
|
984
1094
|
details: [
|
|
985
1095
|
"Run ONLY after the named human explicitly approved in this session (AGENTS.md section 4).",
|
|
@@ -988,7 +1098,7 @@ var COMMANDS = {
|
|
|
988
1098
|
"The launch gate is recorded by hand in config.yaml (its content is the lead set)."
|
|
989
1099
|
],
|
|
990
1100
|
writes: "context/icp.yaml \u2192 approval (icp), or campaigns/<campaign>/config.yaml \u2192 gates.copy (copy).",
|
|
991
|
-
examples: ['
|
|
1101
|
+
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
1102
|
},
|
|
993
1103
|
sync: {
|
|
994
1104
|
usage: "gtm sync [--check]",
|
|
@@ -998,14 +1108,14 @@ var COMMANDS = {
|
|
|
998
1108
|
"--check only compares and exits 1 if they differ (CI uses this; never edit .gtm/ by hand)."
|
|
999
1109
|
],
|
|
1000
1110
|
writes: ".gtm/ (replaced entirely) and AGENTS.md; creates CLAUDE.md if missing. With --check: nothing.",
|
|
1001
|
-
examples: ["
|
|
1111
|
+
examples: ["npx gtm sync", "npx gtm sync --check"]
|
|
1002
1112
|
},
|
|
1003
1113
|
migrate: {
|
|
1004
1114
|
usage: "gtm migrate [--dry-run]",
|
|
1005
1115
|
summary: "upgrade YAML files to current schema versions",
|
|
1006
|
-
details: ["Applies core schema migrations to
|
|
1116
|
+
details: ["Applies core schema migrations to project.yaml, context/*.yaml and campaigns/*/config.yaml, keeping comments."],
|
|
1007
1117
|
writes: "Those YAML files, only if a migration applies. With --dry-run: nothing.",
|
|
1008
|
-
examples: ["
|
|
1118
|
+
examples: ["npx gtm migrate --dry-run", "npx gtm migrate"]
|
|
1009
1119
|
},
|
|
1010
1120
|
upgrade: {
|
|
1011
1121
|
usage: "gtm upgrade [version] [--force]",
|
|
@@ -1015,7 +1125,7 @@ var COMMANDS = {
|
|
|
1015
1125
|
"Refuses while a campaign is in qa or launch, or with uncommitted changes (--force overrides; ask a GTM engineer)."
|
|
1016
1126
|
],
|
|
1017
1127
|
writes: "package.json, lockfile, node_modules, .gtm/, AGENTS.md and migrated YAML files.",
|
|
1018
|
-
examples: ["
|
|
1128
|
+
examples: ["npx gtm upgrade", "npx gtm upgrade 0.3.0"]
|
|
1019
1129
|
},
|
|
1020
1130
|
job: {
|
|
1021
1131
|
usage: "gtm job <name> <campaign>",
|
|
@@ -1025,15 +1135,15 @@ var COMMANDS = {
|
|
|
1025
1135
|
"Not available yet: it exits 1. Per AGENTS.md this is a stop; do not improvise jobs with ad-hoc scripts."
|
|
1026
1136
|
],
|
|
1027
1137
|
writes: "Nothing yet.",
|
|
1028
|
-
examples: ["
|
|
1138
|
+
examples: ["npx gtm job tam q1-uk-cfo"]
|
|
1029
1139
|
}
|
|
1030
1140
|
};
|
|
1031
1141
|
function generalHelp(version) {
|
|
1032
1142
|
const width = Math.max(...Object.keys(COMMANDS).map((c) => c.length));
|
|
1033
1143
|
const lines = Object.entries(COMMANDS).map(([name, c]) => ` ${name.padEnd(width)} ${c.summary}`);
|
|
1034
|
-
return `gtm v${version}: CLI for gtm-brain
|
|
1144
|
+
return `gtm v${version}: CLI for gtm-brain project repos (@veleads/gtm)
|
|
1035
1145
|
|
|
1036
|
-
Usage: gtm <command> [options] (inside a
|
|
1146
|
+
Usage: gtm <command> [options] (inside a project repo: npx gtm <command>)
|
|
1037
1147
|
|
|
1038
1148
|
Commands:
|
|
1039
1149
|
${lines.join("\n")}
|
|
@@ -1047,7 +1157,7 @@ Global options:
|
|
|
1047
1157
|
|
|
1048
1158
|
Exit codes: 0 success, 1 failure or validation errors.
|
|
1049
1159
|
|
|
1050
|
-
For coding agents: inside a
|
|
1160
|
+
For coding agents: inside a project repo, read AGENTS.md first and follow it. Read-only commands
|
|
1051
1161
|
(validate, status, sync --check, migrate --dry-run) are always safe to run.`;
|
|
1052
1162
|
}
|
|
1053
1163
|
function commandHelp(name) {
|
|
@@ -1073,6 +1183,7 @@ var { values, positionals } = parseArgs({
|
|
|
1073
1183
|
root: { type: "string" },
|
|
1074
1184
|
name: { type: "string" },
|
|
1075
1185
|
dir: { type: "string" },
|
|
1186
|
+
slug: { type: "string" },
|
|
1076
1187
|
by: { type: "string" },
|
|
1077
1188
|
notes: { type: "string" },
|
|
1078
1189
|
date: { type: "string" },
|
|
@@ -1109,7 +1220,7 @@ function run() {
|
|
|
1109
1220
|
if (values.help) return help(command === "help" ? args[0] : command);
|
|
1110
1221
|
switch (command) {
|
|
1111
1222
|
case "init":
|
|
1112
|
-
return runInit(args[0], { name: values.name, dir: values.dir, cwd });
|
|
1223
|
+
return runInit(args[0], { name: values.name, dir: values.dir, slug: values.slug, cwd });
|
|
1113
1224
|
case "new-campaign":
|
|
1114
1225
|
return runNewCampaign(root, args[0]);
|
|
1115
1226
|
case "validate":
|