@veleads/gtm 0.3.0 → 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 CHANGED
@@ -5,6 +5,13 @@ All notable changes to `@veleads/gtm`. Each entry is tagged: **no action**, **op
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.4.0] - 2026-10-24
9
+ - **no action** `gtm init . --name "<Name>"` sets up the current folder instead of creating a
10
+ subfolder (slug from the folder name, e.g. `gtm-project-acme` → `acme`, or `--slug`). Built for a
11
+ freshly cloned repo: `.git` is kept (no `git init` inside an existing repository), `README.md` is
12
+ kept, `.gitignore` gets gtm rules appended, `CLAUDE.md` gets `@AGENTS.md`. Any other existing gtm
13
+ file stops init before anything is written; init never overwrites files.
14
+
8
15
  ## [0.3.0] - 2026-10-24
9
16
  - **migration** "Clients" are now **projects**: one private repo = one project.
10
17
  `client.yaml` → `project.yaml`, `CLIENT.md` → `PROJECT.md`, `client_id` → `project_id` in every YAML
package/README.md CHANGED
@@ -12,14 +12,15 @@ Source: [Veleads/gtm-core](https://github.com/Veleads/gtm-core) · MIT
12
12
  Requires Node.js 22+. npm is enough; pnpm or yarn work too (the repo follows whichever lockfile you commit).
13
13
 
14
14
  ```bash
15
- npx @veleads/gtm init acme --name "Acme Corp"
16
- cd gtm-project-acme
15
+ # in an empty or freshly cloned (private) repo folder:
16
+ npx @veleads/gtm init . --name "Acme Corp"
17
17
  npm install
18
18
  npx gtm status
19
19
  npx gtm validate
20
20
  ```
21
21
 
22
- Push the new folder to a **private** repository, then open it in your coding agent. The generated
22
+ `init .` keeps an existing `.git`, README and `.gitignore` (gtm rules are appended) and never overwrites
23
+ files; `init acme --name "Acme Corp"` creates `./gtm-project-acme` instead. Push to a **private** repository, then open it in your coding agent. The generated
23
24
  `AGENTS.md` tells the agent how to work in the repo, starting at stage `00-intake`.
24
25
 
25
26
  ## What you get in a project repo
@@ -43,7 +44,7 @@ command writes**, and examples, without running it.
43
44
 
44
45
  | Command | What it does | Writes |
45
46
  |---|---|---|
46
- | `init <slug> --name "<Name>"` | scaffold a new project repo | new folder |
47
+ | `init . --name "<Name>"` / `init <slug> --name "<Name>"` | set up the current folder / a new one | target folder, never overwrites |
47
48
  | `new-campaign <slug>` | scaffold `campaigns/<slug>/` | that folder |
48
49
  | `validate [campaign]` | schemas, gates, cross-references, no lead data, core sync | nothing |
49
50
  | `status` | campaigns, stages, gates | nothing |
package/dist/cli.js CHANGED
@@ -430,8 +430,8 @@ function runApprove(root2, args2, opts) {
430
430
 
431
431
  // src/commands/init.ts
432
432
  import { spawnSync } from "child_process";
433
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "fs";
434
- 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";
435
435
 
436
436
  // src/paths.ts
437
437
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
@@ -449,7 +449,7 @@ var KIT_DIR = kit;
449
449
  var CHANGELOG_FILE = firstExisting([join2(pkgRoot, "CHANGELOG.md"), join2(pkgRoot, "..", "..", "CHANGELOG.md")]);
450
450
 
451
451
  // src/template.ts
452
- import { cpSync, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
452
+ import { mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
453
453
  import { dirname as dirname2, join as join4 } from "path";
454
454
 
455
455
  // src/fsutil.ts
@@ -505,19 +505,22 @@ function destName(rel) {
505
505
  const [first, ...rest] = rel.split("/");
506
506
  return [DOTFILES.has(first) ? `.${first}` : first, ...rest].join("/");
507
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
+ }
508
522
  function copyTemplate(src, dest, vars, opts = {}) {
509
- for (const rel of walk(src)) {
510
- if (opts.only && !opts.only.includes(rel)) continue;
511
- const out = join4(dest, (opts.rename ?? destName)(rel));
512
- mkdirSync(dirname2(out), { recursive: true });
513
- if (/\.(md|ya?ml|json|txt|example)$|gitignore$/.test(rel)) {
514
- let text = readFileSync3(join4(src, rel), "utf8");
515
- for (const [k, v] of Object.entries(vars)) text = text.replaceAll(`__${k}__`, v);
516
- writeFileSync2(out, text);
517
- } else {
518
- cpSync(join4(src, rel), out);
519
- }
520
- }
523
+ for (const file of planTemplate(src, vars, opts.rename)) writePlanned(dest, file);
521
524
  }
522
525
 
523
526
  // src/commands/sync.ts
@@ -584,33 +587,86 @@ function runSync(root2, opts = {}) {
584
587
  }
585
588
 
586
589
  // src/commands/init.ts
587
- function runInit(slug, opts) {
588
- if (!slug || !Slug.safeParse(slug).success || !opts.name) {
589
- console.error('Usage: gtm init <project-slug> --name "<Project Name>" [--dir <path>] (slug: lowercase, digits, hyphens)');
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`);
590
610
  return false;
591
611
  }
592
- const root2 = resolve(opts.cwd, opts.dir ?? `gtm-project-${slug}`);
593
- if (existsSync4(root2) && readdirSync2(root2).length > 0) {
594
- 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.`);
595
614
  return false;
596
615
  }
597
616
  const vars = { PROJECT_ID: slug, PROJECT_NAME: opts.name, TODAY: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), GTM_VERSION };
598
617
  const tpl = join6(KIT_DIR, "templates");
599
- copyTemplate(join6(tpl, "repo"), root2, vars);
600
- copyTemplate(join6(tpl, "project"), root2, vars, { rename: (rel) => rel === "project.yaml" ? rel : join6("context", rel) });
601
- for (const d of ["campaigns", "tam"]) {
602
- mkdirSync3(join6(root2, d), { recursive: true });
603
- writeFileSync4(join6(root2, d, ".gitkeep"), "");
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;
604
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);
605
655
  runSync(root2);
606
- const git = spawnSync("git", ["init", "-q", "-b", "main"], { cwd: root2 });
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}`);
607
666
  console.log(`
608
- Created ${root2} (core v${GTM_VERSION})${git.status === 0 ? " with git initialised" : ""}.
609
-
610
667
  Next:
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)
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.
614
670
  3. Open the repo in Claude Code or Codex and start with stage 00-intake.`);
615
671
  return true;
616
672
  }
@@ -626,8 +682,8 @@ Per AGENTS.md this is a mandatory stop: do not improvise the job with ad-hoc scr
626
682
  }
627
683
 
628
684
  // src/commands/migrate.ts
629
- import { existsSync as existsSync5, readFileSync as readFileSync5, renameSync, writeFileSync as writeFileSync5 } from "fs";
630
- import { join as join7, relative as relative2 } from "path";
685
+ import { existsSync as existsSync5, readFileSync as readFileSync6, renameSync, writeFileSync as writeFileSync4 } from "fs";
686
+ import { join as join7, relative as relative3 } from "path";
631
687
  import { parseDocument as parseDocument2 } from "yaml";
632
688
  function projectYamlFiles(root2) {
633
689
  const files = [
@@ -651,7 +707,7 @@ function runMigrate(root2, opts = {}) {
651
707
  renameSync(join7(root2, from), join7(root2, to));
652
708
  if (to.endsWith(".md")) {
653
709
  const p = join7(root2, to);
654
- writeFileSync5(p, readFileSync5(p, "utf8").replace(new RegExp(`^# ${from.replace(".", "\\.")}`, "m"), `# ${to}`));
710
+ writeFileSync4(p, readFileSync6(p, "utf8").replace(new RegExp(`^# ${from.replace(".", "\\.")}`, "m"), `# ${to}`));
655
711
  }
656
712
  }
657
713
  if (opts.dryRun && changed) {
@@ -659,8 +715,8 @@ function runMigrate(root2, opts = {}) {
659
715
  return ok;
660
716
  }
661
717
  for (const { kind, file } of projectYamlFiles(root2)) {
662
- const rel = relative2(root2, file);
663
- const doc = parseDocument2(readFileSync5(file, "utf8"));
718
+ const rel = relative3(root2, file);
719
+ const doc = parseDocument2(readFileSync6(file, "utf8"));
664
720
  if (doc.errors.length) {
665
721
  console.log(` \u2717 ${rel}: invalid YAML: ${doc.errors[0].message}`);
666
722
  ok = false;
@@ -677,7 +733,7 @@ function runMigrate(root2, opts = {}) {
677
733
  doc.setIn(["approval", "content_hash"], icpContentHash(doc.toJS()));
678
734
  console.log(` re-pinned ICP approval hash in ${rel} (core migration, content approved by the project approver)`);
679
735
  }
680
- if (!opts.dryRun) writeFileSync5(file, doc.toString({ flowCollectionPadding: false, lineWidth: 0 }));
736
+ if (!opts.dryRun) writeFileSync4(file, doc.toString({ flowCollectionPadding: false, lineWidth: 0 }));
681
737
  } catch (e) {
682
738
  console.log(` \u2717 ${rel}: ${e.message}`);
683
739
  ok = false;
@@ -688,7 +744,7 @@ function runMigrate(root2, opts = {}) {
688
744
  }
689
745
 
690
746
  // src/commands/new-campaign.ts
691
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
747
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
692
748
  import { join as join8 } from "path";
693
749
  import { parse } from "yaml";
694
750
  function runNewCampaign(root2, campaign) {
@@ -706,21 +762,21 @@ function runNewCampaign(root2, campaign) {
706
762
  console.error(`campaigns/${campaign} already exists`);
707
763
  return false;
708
764
  }
709
- const projectId = parse(readFileSync6(projectFile, "utf8"))?.project_id ?? "TODO";
765
+ const projectId = parse(readFileSync7(projectFile, "utf8"))?.project_id ?? "TODO";
710
766
  copyTemplate(join8(KIT_DIR, "templates", "campaign"), dest, { PROJECT_ID: projectId, CAMPAIGN_ID: campaign, OWNER: process.env.GTM_USER ?? "TODO" });
711
- mkdirSync4(join8(dest, "runs"), { recursive: true });
712
- writeFileSync6(join8(dest, "runs", ".gitkeep"), "");
767
+ mkdirSync3(join8(dest, "runs"), { recursive: true });
768
+ writeFileSync5(join8(dest, "runs", ".gitkeep"), "");
713
769
  console.log(`Created campaigns/${campaign}. Branch: git switch -c campaign/${campaign}`);
714
770
  return true;
715
771
  }
716
772
 
717
773
  // src/commands/status.ts
718
- import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
774
+ import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync8 } from "fs";
719
775
  import { join as join9 } from "path";
720
776
  import { parse as parse2 } from "yaml";
721
777
  function readYaml(file) {
722
778
  try {
723
- return parse2(readFileSync7(file, "utf8"));
779
+ return parse2(readFileSync8(file, "utf8"));
724
780
  } catch {
725
781
  return null;
726
782
  }
@@ -737,7 +793,7 @@ function runStatus(root2) {
737
793
  const rows = subdirs(join9(root2, "campaigns")).map((id) => {
738
794
  const cfg = readYaml(join9(root2, "campaigns", id, "config.yaml")) ?? {};
739
795
  const runsDir = join9(root2, "campaigns", id, "runs");
740
- const last = existsSync7(runsDir) ? readdirSync3(runsDir).filter((f) => f.endsWith(".md")).sort().at(-1) ?? "-" : "-";
796
+ const last = existsSync7(runsDir) ? readdirSync2(runsDir).filter((f) => f.endsWith(".md")).sort().at(-1) ?? "-" : "-";
741
797
  return [id, cfg.stage ?? "?", cfg.gates?.copy?.status ?? "?", cfg.gates?.launch?.status ?? "?", cfg.owner ?? "?", last];
742
798
  });
743
799
  if (!rows.length) {
@@ -755,7 +811,7 @@ function runStatus(root2) {
755
811
 
756
812
  // src/commands/upgrade.ts
757
813
  import { spawnSync as spawnSync2 } from "child_process";
758
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
814
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "fs";
759
815
  import { join as join11 } from "path";
760
816
 
761
817
  // src/changelog.ts
@@ -774,7 +830,7 @@ function changelogBetween(text, from, to) {
774
830
  }
775
831
 
776
832
  // src/commands/validate.ts
777
- import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
833
+ import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "fs";
778
834
  import { join as join10 } from "path";
779
835
  import { parse as parse3 } from "yaml";
780
836
  var REQUIRED_CONTEXT = ["intake.md", "icp.md", "icp.yaml", "signals.yaml", "personas.md", "offer.md", "proof.md", "voice.md"];
@@ -787,7 +843,7 @@ var SKIP_DIRS = ["node_modules", ".git", ".gtm"];
787
843
  var stageIndex = (s) => Stage.options.indexOf(s);
788
844
  function readPackageJson(root2) {
789
845
  const p = join10(root2, "package.json");
790
- return existsSync8(p) ? JSON.parse(readFileSync8(p, "utf8")) : null;
846
+ return existsSync8(p) ? JSON.parse(readFileSync9(p, "utf8")) : null;
791
847
  }
792
848
  function pinnedVersion(root2) {
793
849
  const pkg = readPackageJson(root2);
@@ -800,7 +856,7 @@ function loadVersioned(r, file, kind, schema) {
800
856
  }
801
857
  let raw;
802
858
  try {
803
- raw = parse3(readFileSync8(file, "utf8"));
859
+ raw = parse3(readFileSync9(file, "utf8"));
804
860
  } catch (e) {
805
861
  r.err(file, `invalid YAML: ${e.message}`);
806
862
  return null;
@@ -824,7 +880,7 @@ function checkCoreSync(r, root2) {
824
880
  if (!pinned) return;
825
881
  if (!/^\d+\.\d+\.\d+/.test(pinned)) r.err(join10(root2, "package.json"), `${PACKAGE_NAME} must be pinned exactly (got "${pinned}")`);
826
882
  const versionFile = join10(root2, ".gtm", "VERSION");
827
- const synced = existsSync8(versionFile) ? readFileSync8(versionFile, "utf8").trim() : null;
883
+ const synced = existsSync8(versionFile) ? readFileSync9(versionFile, "utf8").trim() : null;
828
884
  if (synced !== GTM_VERSION) r.err(versionFile, `is ${synced ?? "missing"}, installed core is ${GTM_VERSION}: run \`npx gtm sync\``);
829
885
  if (!existsSync8(join10(root2, "AGENTS.md"))) r.err(join10(root2, "AGENTS.md"), "missing: run `npx gtm sync`");
830
886
  }
@@ -836,8 +892,8 @@ function checkNoLeadData(r, root2) {
836
892
  continue;
837
893
  }
838
894
  if (!/\.(md|ya?ml|txt)$/i.test(rel) || rel === "pnpm-lock.yaml") continue;
839
- if (/\.ya?ml$/i.test(rel) && /\bTODO\b/.test(readFileSync8(file, "utf8"))) r.warn(file, "still contains TODO placeholders");
840
- const found = (readFileSync8(file, "utf8").match(EMAIL) ?? []).filter((e) => !ALLOWED_EMAIL.test(e));
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));
841
897
  if (found.length) r.warn(file, `contains email address(es) ${[...new Set(found)].slice(0, 3).join(", ")}: confirm none are prospects`);
842
898
  }
843
899
  }
@@ -882,7 +938,7 @@ function validateCampaign(r, root2, projectId, campaign, icp, signals, icpApprov
882
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`);
883
939
  }
884
940
  const copyFile = join10(dir, "copy.md");
885
- const copyApproved = existsSync8(copyFile) ? checkPinnedApproval(r, copyFile, cfg.gates.copy, copyContentHash(readFileSync8(copyFile, "utf8")), "copy", `npx gtm approve copy ${campaign}`) : false;
941
+ const copyApproved = existsSync8(copyFile) ? checkPinnedApproval(r, copyFile, cfg.gates.copy, copyContentHash(readFileSync9(copyFile, "utf8")), "copy", `npx gtm approve copy ${campaign}`) : false;
886
942
  if (stageIndex(cfg.stage) > stageIndex("copy") && !copyApproved) {
887
943
  r.err(cfgFile, `stage is "${cfg.stage}" but gates.copy is not approved, or copy.md changed since approval`);
888
944
  }
@@ -898,7 +954,7 @@ function validateCampaign(r, root2, projectId, campaign, icp, signals, icpApprov
898
954
  }
899
955
  const runs = join10(dir, "runs");
900
956
  if (existsSync8(runs)) {
901
- for (const f of readdirSync4(runs).filter((f2) => !f2.startsWith("."))) {
957
+ for (const f of readdirSync3(runs).filter((f2) => !f2.startsWith("."))) {
902
958
  if (!RUN_LOG_NAME.test(f)) r.err(join10(runs, f), "run logs must be named YYYY-MM-DD-<user>[-n].md");
903
959
  }
904
960
  }
@@ -923,7 +979,7 @@ function runValidate(root2, onlyCampaign) {
923
979
  const signals = existsSync8(sigFile) ? loadVersioned(r, sigFile, "signals", SignalsFile) : null;
924
980
  if (meta && icp && icp.project_id !== projectId) r.err(icpFile, `project_id "${icp.project_id}" does not match project.yaml "${projectId}"`);
925
981
  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;
982
+ const icpApproved = icp ? checkPinnedApproval(r, icpFile, icp.approval, icpContentHash(parse3(readFileSync9(icpFile, "utf8"))), "ICP", "npx gtm approve icp") : false;
927
983
  checkNoLeadData(r, root2);
928
984
  const campaigns = onlyCampaign ? [onlyCampaign] : subdirs(join10(root2, "campaigns"));
929
985
  for (const c of campaigns) {
@@ -979,7 +1035,7 @@ function runUpgrade(root2, opts) {
979
1035
  const after = pinnedVersion(root2) ?? target;
980
1036
  const changelog = join11(root2, "node_modules", ...PACKAGE_NAME.split("/"), "CHANGELOG.md");
981
1037
  if (existsSync9(changelog)) {
982
- const notes = changelogBetween(readFileSync9(changelog, "utf8"), before, after);
1038
+ const notes = changelogBetween(readFileSync10(changelog, "utf8"), before, after);
983
1039
  console.log(`
984
1040
  === Changes ${before} \u2192 ${after} ===
985
1041
  ${notes || "(no changelog entries)"}
@@ -992,15 +1048,19 @@ ${notes || "(no changelog entries)"}
992
1048
  // src/help.ts
993
1049
  var COMMANDS = {
994
1050
  init: {
995
- usage: 'gtm init <project-slug> --name "<Project Name>" [--dir <path>]',
996
- summary: "scaffold a new project repo",
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 ".")',
997
1053
  details: [
998
1054
  "Creates a complete project repo: project.yaml, context/ templates, campaigns/, tam/, PROJECT.md,",
999
- "CI workflows, .gtm/ (core kit) and AGENTS.md, pinned to this CLI's version, then runs git init.",
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.",
1000
1060
  "Push the result to a PRIVATE repo: it will hold project context."
1001
1061
  ],
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']
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']
1004
1064
  },
1005
1065
  "new-campaign": {
1006
1066
  usage: "gtm new-campaign <campaign-slug>",
@@ -1123,6 +1183,7 @@ var { values, positionals } = parseArgs({
1123
1183
  root: { type: "string" },
1124
1184
  name: { type: "string" },
1125
1185
  dir: { type: "string" },
1186
+ slug: { type: "string" },
1126
1187
  by: { type: "string" },
1127
1188
  notes: { type: "string" },
1128
1189
  date: { type: "string" },
@@ -1159,7 +1220,7 @@ function run() {
1159
1220
  if (values.help) return help(command === "help" ? args[0] : command);
1160
1221
  switch (command) {
1161
1222
  case "init":
1162
- 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 });
1163
1224
  case "new-campaign":
1164
1225
  return runNewCampaign(root, args[0]);
1165
1226
  case "validate":
package/kit/AGENTS.md CHANGED
@@ -182,8 +182,9 @@ Every job prints a `run_id`, counts and cost. Copy those into the run log.
182
182
 
183
183
  ## 9. Onboarding
184
184
 
185
- - **New project:** a GTM engineer runs `npx @veleads/gtm init <slug> --name "<Name>"`, pushes it as a
186
- **private** repo (e.g. `<org>/gtm-project-<slug>`), then stage `00-intake` begins.
185
+ - **New project:** create an empty **private** repo (e.g. `<org>/gtm-project-<slug>`), clone it, and in
186
+ that folder run `npx @veleads/gtm init . --name "<Name>"` (or `init <slug>` to create a new folder).
187
+ Commit, push, then stage `00-intake` begins.
187
188
  - **New campaign:** from up-to-date `main`: `git switch -c campaign/<slug>`,
188
189
  `npx gtm new-campaign <slug>`, fill `brief.md`, set `stage`, follow the routing table.
189
190
  - **New SDR:** read this file and `PROJECT.md`, run `npx gtm status`, then section 2 with a GTM engineer watching.
@@ -13,7 +13,8 @@ Collect everything later stages need, once, in files, so no one has to re-ask th
13
13
  - `context/intake.md` (scaffolded from the core template by `gtm init`).
14
14
 
15
15
  ## Steps
16
- 1. If the project repo does not exist yet, a GTM engineer creates it: `npx @veleads/gtm init <slug> --name "<Name>"`, then pushes it as a **private** repo (e.g. `<org>/gtm-project-<slug>`).
16
+ 1. If the project repo does not exist yet, a GTM engineer creates it: an empty **private** repo, cloned,
17
+ then `npx @veleads/gtm init . --name "<Name>"` inside it (keeps `.git` and README).
17
18
  2. Fill `project.yaml`: owners (GTM engineer, SDRs, **project approver**), regions, status `onboarding`.
18
19
  3. Walk the questionnaire in `context/intake.md` with the human. For each answer, write what the
19
20
  stakeholders said, not your interpretation. Unknown → write `unknown` and add it to open questions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veleads/gtm",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "gtm-brain core: process kit (AGENTS.md, skills, templates, schemas) and the gtm CLI for client repos",
5
5
  "keywords": [
6
6
  "gtm",