@delorenj/pjangler 1.5.11 → 1.5.13

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.
Files changed (25) hide show
  1. package/.mise/scripts/versioning.sh +26 -5
  2. package/README.md +25 -0
  3. package/dist/index.js +169 -36
  4. package/dist/mcp-server.js +153 -29
  5. package/dist/notebook/journal-service-main.js +4 -1
  6. package/dist/project-registry-service.js +1 -1
  7. package/package.json +1 -1
  8. package/templates/commonproject/copier.yml +102 -1
  9. package/templates/commonproject/template/.github/workflows/{% if is_obsidian_plugin %}lint.yml{% endif %} +29 -0
  10. package/templates/commonproject/template/.github/workflows/{% if is_obsidian_plugin %}release.yml{% endif %} +62 -0
  11. package/templates/commonproject/template/.mise/{% if is_obsidian_plugin %}tasks{% endif %}/plugin/link +27 -0
  12. package/templates/commonproject/template/AGENTS.md.jinja +67 -0
  13. package/templates/commonproject/template/{% if is_obsidian_plugin %}.editorconfig{% endif %} +15 -0
  14. package/templates/commonproject/template/{% if is_obsidian_plugin %}.npmrc{% endif %} +1 -0
  15. package/templates/commonproject/template/{% if is_obsidian_plugin %}esbuild.config.mjs{% endif %} +49 -0
  16. package/templates/commonproject/template/{% if is_obsidian_plugin %}eslint.config.mts{% endif %} +38 -0
  17. package/templates/commonproject/template/{% if is_obsidian_plugin %}manifest.json{% endif %}.jinja +9 -0
  18. package/templates/commonproject/template/{% if is_obsidian_plugin %}package-lock.json{% endif %}.jinja +5053 -0
  19. package/templates/commonproject/template/{% if is_obsidian_plugin %}package.json{% endif %}.jinja +28 -0
  20. package/templates/commonproject/template/{% if is_obsidian_plugin %}src{% endif %}/main.ts.jinja +105 -0
  21. package/templates/commonproject/template/{% if is_obsidian_plugin %}src{% endif %}/settings.ts.jinja +38 -0
  22. package/templates/commonproject/template/{% if is_obsidian_plugin %}styles.css{% endif %} +8 -0
  23. package/templates/commonproject/template/{% if is_obsidian_plugin %}tsconfig.json{% endif %} +19 -0
  24. package/templates/commonproject/template/{% if is_obsidian_plugin %}version-bump.mjs{% endif %} +17 -0
  25. package/templates/commonproject/template/{% if is_obsidian_plugin %}versions.json{% endif %} +3 -0
@@ -16,10 +16,12 @@
16
16
  # versioning.sh files List the manifest
17
17
  #
18
18
  # Manifest: .mise/version-files.conf — lines of "<type> <path>" (see file-types).
19
- # types: json toml cargo csproj gradle plain gittag
19
+ # types: json toml cargo csproj gradle plain gittag obsidian
20
20
  #
21
21
  # Storage format per type:
22
22
  # json/toml/cargo/csproj/gradle/plain -> bare "X.Y.Z"
23
+ # obsidian (manifest.json) -> bare "X.Y.Z", plus the sibling
24
+ # versions.json gains X.Y.Z -> minAppVersion
23
25
  # gittag -> tag "vX.Y.Z"
24
26
  # `current` always prints with the leading "v".
25
27
 
@@ -85,12 +87,21 @@ manifest_entries() {
85
87
  done <"$MANIFEST"
86
88
  }
87
89
 
90
+ # Rewrite a JSON file through jq, keeping its indentation: tab-indented files
91
+ # (Obsidian's manifest.json/versions.json/package.json) stay tab-indented.
92
+ jq_rewrite() { # $1=path, remaining args go to jq
93
+ local path="$1" tmp indent=(--indent 2); shift
94
+ grep -q $'^\t' "$path" && indent=(--tab)
95
+ tmp="$(mktemp)"
96
+ jq "${indent[@]}" "$@" "$path" >"$tmp" && mv "$tmp" "$path"
97
+ }
98
+
88
99
  # ---- per-type read -----------------------------------------------------------
89
100
 
90
101
  read_version() { # $1=type $2=path -> bare X.Y.Z on stdout, or nothing
91
102
  local type="$1" path="$2" raw=""
92
103
  case "$type" in
93
- json)
104
+ json|obsidian)
94
105
  [[ -f "$path" ]] || return 0
95
106
  raw="$(jq -r '.version // empty' "$path" 2>/dev/null || true)" ;;
96
107
  toml|cargo)
@@ -119,12 +130,22 @@ read_version() { # $1=type $2=path -> bare X.Y.Z on stdout, or nothing
119
130
  # ---- per-type write ----------------------------------------------------------
120
131
 
121
132
  write_version() { # $1=type $2=path $3=bare-new-version
122
- local type="$1" path="$2" new="$3" tmp
133
+ local type="$1" path="$2" new="$3"
123
134
  case "$type" in
124
135
  json)
125
136
  [[ -f "$path" ]] || return 0
126
- tmp="$(mktemp)"
127
- jq --indent 2 --arg v "$new" '.version = $v' "$path" >"$tmp" && mv "$tmp" "$path" ;;
137
+ jq_rewrite "$path" --arg v "$new" '.version = $v' ;;
138
+ obsidian)
139
+ # What the sample plugin's version-bump.mjs does: set the manifest
140
+ # version, then record new -> minAppVersion in versions.json (once).
141
+ [[ -f "$path" ]] || return 0
142
+ jq_rewrite "$path" --arg v "$new" '.version = $v'
143
+ local versions min
144
+ versions="$(dirname "$path")/versions.json"
145
+ min="$(jq -r '.minAppVersion // empty' "$path")"
146
+ if [[ -f "$versions" && -n "$min" ]]; then
147
+ jq_rewrite "$versions" --arg v "$new" --arg m "$min" 'if has($v) then . else .[$v] = $m end'
148
+ fi ;;
128
149
  toml|cargo)
129
150
  [[ -f "$path" ]] || return 0
130
151
  # Replace only the first top-level `version = "..."` line.
package/README.md CHANGED
@@ -76,6 +76,31 @@ instead of reading a clean report over nothing.
76
76
  live catalog. The rules are owned by the recipes in `src/recipes/`, not by a
77
77
  central table, so that catalog is the only authority on which ones exist.
78
78
 
79
+ ## Obsidian plugins
80
+
81
+ `pjangler init <name> --obsidian-plugin` does everything a normal init does
82
+ (registry, `.project.json`, board, BMAD, skills, hooks, mise, the init commit).
83
+ The difference is the CommonProject render: it is the variant built on the
84
+ canonical [obsidian-sample-plugin](https://github.com/obsidianmd/obsidian-sample-plugin).
85
+ That means `manifest.json`, `versions.json`, npm + esbuild + TypeScript in `src/`,
86
+ `eslint-plugin-obsidianmd`, and build/release workflows on the self-hosted runners.
87
+
88
+ ```bash
89
+ pjangler init tag-wrangler --obsidian-plugin --description "Wrangle tags across a vault"
90
+ cd tag-wrangler && npm install && npm run dev
91
+ mise run plugin:link ~/Vaults/Main # symlink into <vault>/.obsidian/plugins/<id>
92
+ ```
93
+
94
+ - The type is recorded as `template.commonproject.project_type` and in
95
+ `.copier-answers.yml`. It is fixed at creation, and re-running `init` keeps it.
96
+ - On an existing repo, the flag only records the type, and only when the repo
97
+ already has an Obsidian `manifest.json`.
98
+ - `mise run version:bump` keeps `package.json`, `manifest.json` and `versions.json`
99
+ in step. It never tags. Release with `npm version patch|minor|major`, which
100
+ commits and then tags a bare `X.Y.Z` (Obsidian matches that), and push the tag.
101
+ - MCP takes the same choice as `projectType: "obsidian-plugin"` on
102
+ `pjangler_project_init` and `pjangler_bootstrap_33god_project`.
103
+
79
104
  ## Agents
80
105
 
81
106
  pjangler does not hire, provision, or inspect agents. That is Flume's surface —
package/dist/index.js CHANGED
@@ -968,6 +968,38 @@ import { homedir as homedir5, tmpdir } from "node:os";
968
968
  import { basename as basename7, delimiter as delimiter2, dirname as dirname11, isAbsolute as isAbsolute5, join as join12, relative as relative7, resolve as resolve10, sep as sep2, win32 } from "node:path";
969
969
  import { fileURLToPath as fileURLToPath2 } from "node:url";
970
970
  import YAML4 from "yaml";
971
+ function normalizeProjectType(value, label = "Project type") {
972
+ const text2 = typeof value === "string" ? value.trim().toLowerCase() : "";
973
+ if (PROJECT_TYPES.includes(text2)) return text2;
974
+ throw new Error(`${label} ${JSON.stringify(value)} is not one of: ${PROJECT_TYPES.join(", ")}`);
975
+ }
976
+ function looksLikeObsidianPlugin(dir) {
977
+ try {
978
+ const manifest = JSON.parse(readFileSync10(join12(dir, "manifest.json"), "utf8"));
979
+ return isRecord3(manifest) && typeof manifest.id === "string" && typeof manifest.minAppVersion === "string";
980
+ } catch {
981
+ return false;
982
+ }
983
+ }
984
+ function assertTemplateOffersProjectType(pjanglerRoot, projectType) {
985
+ if (projectType === "base") return;
986
+ const copierYml = join12(pjanglerRoot, "templates", "commonproject", "copier.yml");
987
+ let choices;
988
+ try {
989
+ const parsed = YAML4.parse(readFileSync10(copierYml, "utf8"));
990
+ choices = isRecord3(parsed) && isRecord3(parsed.project_type) ? parsed.project_type.choices : void 0;
991
+ } catch {
992
+ choices = void 0;
993
+ }
994
+ const offered = isRecord3(choices) ? Object.values(choices) : Array.isArray(choices) ? choices : [];
995
+ if (!offered.includes(projectType)) {
996
+ throw new Error(`The CommonProject template at ${dirname11(copierYml)} cannot render project type "${projectType}" (its copier.yml offers no such project_type); update pjangler or its templates/commonproject pin`);
997
+ }
998
+ }
999
+ function operatorDisplayName(cwd) {
1000
+ const result2 = spawnSync5("git", ["config", "--get", "user.name"], { cwd: existsSync7(cwd) ? cwd : void 0, encoding: "utf8", timeout: 5e3 });
1001
+ return result2.status === 0 ? result2.stdout.trim() : "";
1002
+ }
971
1003
  function providerAssignsIdentifiers(type) {
972
1004
  return IDENTIFIER_ASSIGNING_PROVIDERS.includes((type ?? "").trim());
973
1005
  }
@@ -1560,6 +1592,26 @@ function planProjectInit(input) {
1560
1592
  const overwrite = input.overwrite ?? input.force ?? false;
1561
1593
  const agents = createSafeRecord(Object.entries(existing?.agents ?? {}));
1562
1594
  const scaffold = input.scaffold ?? true;
1595
+ const recordedTypeValue = existing?.template?.commonproject?.project_type;
1596
+ const recordedType = recordedTypeValue === void 0 ? void 0 : normalizeProjectType(recordedTypeValue, `Recorded project type of ${slug}`);
1597
+ const requestedType = input.projectType === void 0 ? void 0 : normalizeProjectType(input.projectType);
1598
+ const freshRender = scaffold && localManifest === void 0 && !existsSync7(join12(targetDir, ".copier-answers.yml"));
1599
+ if (requestedType && !freshRender && requestedType !== (recordedType ?? "base")) {
1600
+ const adoptingPlugin = !scaffold && recordedType === void 0 && requestedType === "obsidian-plugin" && looksLikeObsidianPlugin(targetDir);
1601
+ if (!adoptingPlugin) {
1602
+ throw new Error(scaffold || recordedType !== void 0 ? `Project type "${requestedType}" conflicts with ${slug}'s recorded type "${recordedType ?? "base"}"; a project's type is fixed when it is created` : `${targetDir} is adopted as-is, so "${requestedType}" would render nothing, and the repo is not one already (no Obsidian manifest.json with id and minAppVersion)`);
1603
+ }
1604
+ }
1605
+ const projectType = requestedType ?? recordedType ?? "base";
1606
+ const typeLanguages = PROJECT_TYPE_LANGUAGES[projectType];
1607
+ if (input.primaryLanguage !== void 0 && typeLanguages && !typeLanguages.includes(input.primaryLanguage)) {
1608
+ throw new Error(`A ${projectType} project is written in ${typeLanguages.join(" or ")}, not ${input.primaryLanguage}`);
1609
+ }
1610
+ const recordedLanguageValue = (localManifest ? localManifest.template : indexed?.template)?.commonproject?.primary_language;
1611
+ const recordedLanguage = typeof recordedLanguageValue === "string" && recordedLanguageValue ? recordedLanguageValue : void 0;
1612
+ const newlyTyped = requestedType !== void 0 && requestedType !== recordedType;
1613
+ const recordedLanguageFits = !typeLanguages || recordedLanguage !== void 0 && typeLanguages.includes(recordedLanguage);
1614
+ const primaryLanguage = input.primaryLanguage ?? (newlyTyped && !recordedLanguageFits ? typeLanguages?.[0] : void 0) ?? recordedLanguage ?? typeLanguages?.[0] ?? "python";
1563
1615
  const candidateProject = {
1564
1616
  ...existing ?? {},
1565
1617
  name: input.name,
@@ -1581,7 +1633,10 @@ function planProjectInit(input) {
1581
1633
  commonproject: {
1582
1634
  ...existing?.template?.commonproject,
1583
1635
  enabled: true,
1584
- primary_language: input.primaryLanguage ?? existing?.template?.commonproject?.primary_language ?? "python"
1636
+ primary_language: primaryLanguage,
1637
+ // Written only for a non-base type (or one already recorded), so a
1638
+ // base record never gains a key and stays equivalent to its last run.
1639
+ ...projectType !== "base" || recordedType !== void 0 ? { project_type: projectType } : {}
1585
1640
  }
1586
1641
  },
1587
1642
  ticket_provider: { ...existing?.ticket_provider, ...buildTicketProviderBlock({
@@ -1617,6 +1672,7 @@ function planProjectInit(input) {
1617
1672
  { kind: "registry.upsert", registryPath, slug, project }
1618
1673
  ];
1619
1674
  if (scaffold) {
1675
+ assertTemplateOffersProjectType(pjanglerRoot, projectType);
1620
1676
  actions.push(buildCommonProjectCopierAction({
1621
1677
  pjanglerRoot,
1622
1678
  targetDir,
@@ -1630,6 +1686,8 @@ function planProjectInit(input) {
1630
1686
  boardId: project.ticket_provider.board_id ?? "",
1631
1687
  projectIdentifier: identifier,
1632
1688
  primaryLanguage: project.template.commonproject.primary_language,
1689
+ projectType,
1690
+ ...projectType === "obsidian-plugin" ? { pluginAuthor: operatorDisplayName(input.cwd ?? process.cwd()) } : {},
1633
1691
  agentHooksLayer: resolveAgentHooksLayer2(input.agentHooksLayer),
1634
1692
  overwrite
1635
1693
  }));
@@ -1768,6 +1826,13 @@ async function executeProjectInitPlan(plan, options = {}) {
1768
1826
  errors.push(`copier exited with status ${result2.status ?? "unknown"}`);
1769
1827
  break;
1770
1828
  }
1829
+ const projectType = action.data.project_type ?? "base";
1830
+ const missing = (PROJECT_TYPE_SENTINELS[projectType] ?? []).filter((rel) => !existsSync7(join12(action.targetDir, rel)));
1831
+ if (missing.length) {
1832
+ errors.push(`copier rendered no ${projectType} project: ${missing.join(", ")} missing from ${action.targetDir}`);
1833
+ break;
1834
+ }
1835
+ if (projectType !== "base") logs.push(`commonproject: rendered as ${projectType}`);
1771
1836
  } else if (action.kind === "project.write-manifest") {
1772
1837
  if (action.expectedContent !== void 0 && (!existsSync7(action.path) || readFileSync10(action.path, "utf8") !== action.expectedContent)) {
1773
1838
  errors.push(`Manifest changed after planning: ${action.path}; re-plan before applying`);
@@ -1887,13 +1952,18 @@ function formatProjectInitPlan(plan) {
1887
1952
  lines.push(` ${cyan(bold(glyph.chevron))} ${title}${plan.dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
1888
1953
  lines.push(` ${dim("registry".padEnd(8))} ${dim(plan.registryPath)}`);
1889
1954
  lines.push(` ${dim("target".padEnd(8))} ${dim(plan.project.repo_path)}`);
1955
+ const projectType = plan.project.template?.commonproject?.project_type;
1956
+ if (projectType && projectType !== "base") lines.push(` ${dim("type".padEnd(8))} ${cyan(projectType)}`);
1890
1957
  for (const warning of plan.warnings ?? []) lines.push(` ${yellow(glyph.warn)} ${warning}`);
1891
1958
  lines.push("");
1892
1959
  lines.push(` ${bold("Actions")} ${dim(`(${plan.actions.length})`)}`);
1893
1960
  if (!plan.actions.length) lines.push(` ${dim("(nothing to do)")}`);
1894
1961
  for (const action of plan.actions) {
1895
1962
  lines.push(` ${cyan(glyph.bullet)} ${action.kind}`);
1896
- if (action.kind === "copier.copy.commonproject") lines.push(` ${dim(`target: ${action.targetDir}`)}`);
1963
+ if (action.kind === "copier.copy.commonproject") {
1964
+ lines.push(` ${dim(`target: ${action.targetDir}`)}`);
1965
+ if (action.data.project_type && action.data.project_type !== "base") lines.push(` ${dim(`type: ${action.data.project_type}`)}`);
1966
+ }
1897
1967
  if (action.kind === "project.write-manifest") lines.push(` ${dim(`path: ${action.path}`)}`);
1898
1968
  if (action.kind === "ticket-provider.create-or-link") {
1899
1969
  const target = [action.provider, action.workspace, action.identifier].filter(Boolean).join("/");
@@ -2014,6 +2084,8 @@ function buildCommonProjectCopierAction(input) {
2014
2084
  board_id: input.boardId ?? input.planeProjectId ?? "",
2015
2085
  project_identifier: input.projectIdentifier,
2016
2086
  primary_language: input.primaryLanguage,
2087
+ project_type: input.projectType ?? "base",
2088
+ ...input.pluginAuthor !== void 0 ? { plugin_author: input.pluginAuthor } : {},
2017
2089
  agent_hooks_layer: input.agentHooksLayer ?? true ? "true" : "false"
2018
2090
  };
2019
2091
  const command = ["copier", "copy", "--trust", "--vcs-ref=HEAD", templateDir, input.targetDir, "--defaults"];
@@ -2114,7 +2186,7 @@ function expandHome(path) {
2114
2186
  function isRecord3(value) {
2115
2187
  return typeof value === "object" && value !== null && !Array.isArray(value);
2116
2188
  }
2117
- var PROJECT_SOURCE_SKILL_ROOTS_ENV, TICKET_PROVIDER_ADAPTERS_ENV, PROJECT_REGISTRY_SCHEMA_VERSION, DEFAULT_NEW_PROJECT_STATUS, BOARD_URL_DEPRECATION_WARNING, PROJECT_IDENTIFIER_SOURCES, IDENTIFIER_ASSIGNING_PROVIDERS, TICKET_PROVIDER_STATES, IDENTIFIER_REPAIR_COMMAND, DEFAULT_SOURCE_SKILL_ROOTS, PROJECT_REGISTRY_OWNED_KEYS, PROJECT_NOTEBOOK_OWNED_KEYS, TICKET_PROVIDER_OWNED_KEYS, GLOBAL_NOTEBOOK_OWNED_KEYS, GLOBAL_NOTEBOOK_AUTH_OWNED_KEYS, GLOBAL_NOTEBOOK_DEFAULTS_OWNED_KEYS, GLOBAL_NOTEBOOK_LIMITS_OWNED_KEYS, GLOBAL_NOTEBOOK_SUMMARIZER_OWNED_KEYS, SAFE_PATH_SEGMENT;
2189
+ var PROJECT_SOURCE_SKILL_ROOTS_ENV, TICKET_PROVIDER_ADAPTERS_ENV, PROJECT_REGISTRY_SCHEMA_VERSION, DEFAULT_NEW_PROJECT_STATUS, PROJECT_TYPES, PROJECT_TYPE_LANGUAGES, PROJECT_TYPE_SENTINELS, BOARD_URL_DEPRECATION_WARNING, PROJECT_IDENTIFIER_SOURCES, IDENTIFIER_ASSIGNING_PROVIDERS, TICKET_PROVIDER_STATES, IDENTIFIER_REPAIR_COMMAND, DEFAULT_SOURCE_SKILL_ROOTS, PROJECT_REGISTRY_OWNED_KEYS, PROJECT_NOTEBOOK_OWNED_KEYS, TICKET_PROVIDER_OWNED_KEYS, GLOBAL_NOTEBOOK_OWNED_KEYS, GLOBAL_NOTEBOOK_AUTH_OWNED_KEYS, GLOBAL_NOTEBOOK_DEFAULTS_OWNED_KEYS, GLOBAL_NOTEBOOK_LIMITS_OWNED_KEYS, GLOBAL_NOTEBOOK_SUMMARIZER_OWNED_KEYS, SAFE_PATH_SEGMENT;
2118
2190
  var init_project = __esm({
2119
2191
  "src/project/index.ts"() {
2120
2192
  "use strict";
@@ -2130,6 +2202,13 @@ var init_project = __esm({
2130
2202
  TICKET_PROVIDER_ADAPTERS_ENV = "PJ_TICKET_PROVIDER_ADAPTERS";
2131
2203
  PROJECT_REGISTRY_SCHEMA_VERSION = 1;
2132
2204
  DEFAULT_NEW_PROJECT_STATUS = "active";
2205
+ PROJECT_TYPES = ["base", "obsidian-plugin"];
2206
+ PROJECT_TYPE_LANGUAGES = {
2207
+ "obsidian-plugin": ["typescript", "javascript"]
2208
+ };
2209
+ PROJECT_TYPE_SENTINELS = {
2210
+ "obsidian-plugin": ["manifest.json", join12("src", "main.ts")]
2211
+ };
2133
2212
  BOARD_URL_DEPRECATION_WARNING = "boardUrl is deprecated and ignored; board URLs are derived at runtime and are never persisted.";
2134
2213
  PROJECT_IDENTIFIER_SOURCES = ["provider", "proposed"];
2135
2214
  IDENTIFIER_ASSIGNING_PROVIDERS = ["plane", "linear"];
@@ -8510,9 +8589,24 @@ function lstatIfPresent(path) {
8510
8589
  }
8511
8590
  }
8512
8591
  function templateVersionFilesConf(ctx, repoRoot) {
8592
+ if (isObsidianPluginRepo(ctx, repoRoot)) return OBSIDIAN_VERSION_FILES_CONF;
8513
8593
  const packageJson = join5(repoRoot, "package.json");
8514
8594
  return existsSync3(packageJson) ? "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\njson package.json\ngittag .\n" : "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\ngittag .\n";
8515
8595
  }
8596
+ var OBSIDIAN_VERSION_FILES_CONF = "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag obsidian\n# Obsidian plugin: bump with `mise run version:bump`, release with `npm version` (bare X.Y.Z tag).\njson package.json\nobsidian manifest.json\n";
8597
+ function recordedProjectType(ctx) {
8598
+ return record(record(readProjectJson(ctx)?.template).commonproject).project_type;
8599
+ }
8600
+ function isObsidianPluginRepo(ctx, repoRoot) {
8601
+ const recorded = recordedProjectType(ctx);
8602
+ if (recorded !== void 0) return recorded === "obsidian-plugin";
8603
+ const manifest = record(tryParseJson(safeReadText2(join5(repoRoot, "manifest.json"))));
8604
+ return typeof manifest.id === "string" && typeof manifest.minAppVersion === "string";
8605
+ }
8606
+ function unsupportedVersionTypes(conf, script) {
8607
+ const types = conf.split("\n").map((line) => line.trim().split(/\s+/)[0] ?? "").filter((type) => type && !type.startsWith("#"));
8608
+ return [...new Set(types)].filter((type) => !new RegExp(`(^|[\\s|])${type.replace(/[^a-z0-9-]/gi, "")}(\\|[a-z-]+)*\\)`, "m").test(script));
8609
+ }
8516
8610
  var BASE_MISE_PATH_ENTRIES = [".mise/scripts", "agents/hermes/pm"];
8517
8611
  function requiredMisePathEntries(_ctx) {
8518
8612
  return [...BASE_MISE_PATH_ENTRIES];
@@ -9170,8 +9264,9 @@ function projectJsonFinding(ctx) {
9170
9264
  };
9171
9265
  }
9172
9266
  var BMAD_NPM_PACKAGE = "bmad-method";
9173
- var BMAD_INSTALLER_VERSION = "6.12.0";
9267
+ var BMAD_INSTALLER_VERSION = "6.12.1";
9174
9268
  var BMAD_TARGET_CHANNEL = "next";
9269
+ var BMAD_STABLE_CHANNEL = "latest";
9175
9270
  var BMAD_DIST_TAGS_TTL_MS = 60 * 60 * 1e3;
9176
9271
  var DEFAULT_BMAD_MODULES = ["bmm", "bmb", "cis"];
9177
9272
  var BMAD_INSTALL_TOOLS = SUPPORTED_BMAD_TOOLS;
@@ -9365,10 +9460,17 @@ function runBmadInstall(repoRoot, modules = selectedBmadModules(repoRoot), versi
9365
9460
  const invocation = bmadInstallerInvocation(version);
9366
9461
  const result2 = spawnSync3(invocation.command, [...invocation.prefixArgs, ...bmadInstallerArgs(repoRoot, modules)], { encoding: "utf8" });
9367
9462
  if (result2.status !== 0) {
9368
- return { ok: false, error: result2.stderr || result2.error?.message || "Unknown error" };
9463
+ const stdoutTail = result2.stdout?.trim().split("\n").slice(-12).join("\n");
9464
+ return { ok: false, error: result2.stderr?.trim() || stdoutTail || result2.error?.message || `installer exited ${result2.status}` };
9369
9465
  }
9370
9466
  return { ok: true };
9371
9467
  }
9468
+ function bmadChannelTarget(distTags) {
9469
+ const next = distTags?.[BMAD_TARGET_CHANNEL];
9470
+ const stable = distTags?.[BMAD_STABLE_CHANNEL];
9471
+ if (next && stable && compareBmadVersions(stable, next) > 0) return { version: stable, channel: BMAD_STABLE_CHANNEL };
9472
+ return next ? { version: next, channel: BMAD_TARGET_CHANNEL } : void 0;
9473
+ }
9372
9474
  function readInstalledBmadVersion(repoRoot) {
9373
9475
  const raw = safeReadText2(join5(repoRoot, "_bmad", "_config", "manifest.yaml"));
9374
9476
  if (!raw) return void 0;
@@ -10068,6 +10170,15 @@ function createMiseChecks() {
10068
10170
  if (!text2?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
10069
10171
  if (!existsSync3(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
10070
10172
  if (!existsSync3(manifestPath)) details.push(".mise/version-files.conf missing");
10173
+ const conf = safeReadText2(manifestPath);
10174
+ const script = safeReadText2(versioningPath);
10175
+ if (conf !== null && script !== null) {
10176
+ const unsupported = unsupportedVersionTypes(conf, script);
10177
+ if (unsupported.length) details.push(`.mise/scripts/versioning.sh cannot version type(s) ${unsupported.join(", ")} named in .mise/version-files.conf`);
10178
+ }
10179
+ if (conf !== null && isObsidianPluginRepo(ctx, ctx.repoRoot) && !/^obsidian\s+manifest\.json\s*$/m.test(conf)) {
10180
+ details.push(".mise/version-files.conf does not version manifest.json/versions.json for this obsidian-plugin project");
10181
+ }
10071
10182
  return {
10072
10183
  id: "mise.versioning",
10073
10184
  title: "managed mise versioning block",
@@ -10609,6 +10720,15 @@ function strayOpInjectLines(text2) {
10609
10720
  }
10610
10721
  return [...new Set(lines)].sort((a, b) => a - b);
10611
10722
  }
10723
+ var COPIER_ANSWERS_HEADER = "# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY";
10724
+ function parseCopierAnswers(text2) {
10725
+ try {
10726
+ const parsed = YAML2.parse(text2);
10727
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { answers: parsed } : { problem: ".copier-answers.yml is not a YAML mapping" };
10728
+ } catch (error) {
10729
+ return { problem: `.copier-answers.yml is not valid YAML (${errorText(error).split("\n")[0]})` };
10730
+ }
10731
+ }
10612
10732
  function createProjectProvenanceChecks() {
10613
10733
  return [
10614
10734
  {
@@ -10622,16 +10742,14 @@ function createProjectProvenanceChecks() {
10622
10742
  if (!text2) {
10623
10743
  details.push(".copier-answers.yml missing");
10624
10744
  } else {
10625
- if (!text2.startsWith("# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY")) details.push("missing Copier overwrite warning header");
10626
- if (!text2.includes("_src_path:")) details.push("_src_path missing");
10627
- if (project?.project_name) {
10628
- const nameMatch = text2.match(/project_name:\s*(.+)/);
10629
- if (!nameMatch || nameMatch[1]?.trim() !== String(project.project_name)) details.push("project_name drift between .copier-answers.yml and .project.json");
10630
- }
10631
- if (project?.project_description) {
10632
- const descMatch = text2.match(/project_description:\s*([\s\S]*?)(?=\n\w|$)/);
10633
- const yamlDesc = descMatch?.[1]?.replace(/\n\s+/g, " ").trim() ?? "";
10634
- if (yamlDesc !== String(project.project_description)) details.push("project_description drift between .copier-answers.yml and .project.json");
10745
+ if (!text2.startsWith(COPIER_ANSWERS_HEADER)) details.push("missing Copier overwrite warning header");
10746
+ const parsed = parseCopierAnswers(text2);
10747
+ if ("problem" in parsed) details.push(parsed.problem);
10748
+ else {
10749
+ const { answers } = parsed;
10750
+ if (answers._src_path === void 0) details.push("_src_path missing");
10751
+ if (project?.project_name && String(answers.project_name ?? "") !== String(project.project_name)) details.push("project_name drift between .copier-answers.yml and .project.json");
10752
+ if (project?.project_description && String(answers.project_description ?? "") !== String(project.project_description)) details.push("project_description drift between .copier-answers.yml and .project.json");
10635
10753
  }
10636
10754
  }
10637
10755
  return {
@@ -10646,14 +10764,18 @@ function createProjectProvenanceChecks() {
10646
10764
  migrate: (ctx, finding2) => {
10647
10765
  const changedFiles = [];
10648
10766
  const project = canonicalProjectJson(ctx);
10649
- const text2 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
10650
- _src_path: ${join5(ctx.pjanglerRoot, "templates", "commonproject")}
10651
- project_description: ${String(project.project_description)}
10652
- project_name: ${String(project.project_name)}
10653
- ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
10654
- `;
10655
10767
  const path = join5(ctx.repoRoot, ".copier-answers.yml");
10656
- if (safeReadText2(path) !== text2) {
10768
+ const current = safeReadText2(path);
10769
+ const existing = current ? YAML2.parseDocument(current) : void 0;
10770
+ const document = existing && !existing.errors.length && YAML2.isMap(existing.contents) ? existing : new YAML2.Document({});
10771
+ document.set("_src_path", join5(ctx.pjanglerRoot, "templates", "commonproject"));
10772
+ document.set("project_description", String(project.project_description));
10773
+ document.set("project_name", String(project.project_name));
10774
+ document.set("ticket_provider", String(project.ticket_provider?.type ?? "plane"));
10775
+ const body = String(document);
10776
+ const text2 = body.startsWith(COPIER_ANSWERS_HEADER) ? body : `${COPIER_ANSWERS_HEADER}
10777
+ ${body}`;
10778
+ if (current !== text2) {
10657
10779
  changedFiles.push(path);
10658
10780
  if (!ctx.dryRun) writeText(path, text2);
10659
10781
  }
@@ -10885,7 +11007,9 @@ function createBmadChecks() {
10885
11007
  }
10886
11008
  const pinned = ctx.bmadVersionPin?.trim();
10887
11009
  const resolved = pinned ? void 0 : resolveBmadDistTags(ctx.homeDir);
10888
- const available = pinned ?? resolved?.distTags?.[BMAD_TARGET_CHANNEL];
11010
+ const channelTarget = pinned ? void 0 : bmadChannelTarget(resolved?.distTags);
11011
+ const available = pinned ?? channelTarget?.version;
11012
+ const channel = channelTarget?.channel ?? BMAD_TARGET_CHANNEL;
10889
11013
  if (!available) {
10890
11014
  return {
10891
11015
  id: "bmad.version",
@@ -10896,7 +11020,7 @@ function createBmadChecks() {
10896
11020
  fixable: false
10897
11021
  };
10898
11022
  }
10899
- const targetLabel = pinned ? `pinned ${available}` : `${BMAD_TARGET_CHANNEL} ${available}`;
11023
+ const targetLabel = pinned ? `pinned ${available}` : `${channel} ${available}`;
10900
11024
  const staleNote = resolved?.stale ? ` ${glyph.dot} cached` : "";
10901
11025
  const comparison = compareBmadVersions(installed, available);
10902
11026
  if (pinned ? comparison === 0 : comparison >= 0) {
@@ -10917,7 +11041,7 @@ function createBmadChecks() {
10917
11041
  title: "BMAD version currency",
10918
11042
  status: "skip",
10919
11043
  summary: `BMAD ${installed} installed; stable currency floor unknown (${BMAD_NPM_PACKAGE} latest unresolved)`,
10920
- details: [`installed: ${installed}`, `available: ${available} (${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL})`],
11044
+ details: [`installed: ${installed}`, `available: ${available} (${BMAD_NPM_PACKAGE}@${channel})`],
10921
11045
  fixable: false
10922
11046
  };
10923
11047
  }
@@ -10930,7 +11054,7 @@ function createBmadChecks() {
10930
11054
  summary: `BMAD ${installed} is at or ahead of stable ${stable ?? "unknown"}; ${targetLabel} available${staleNote}`,
10931
11055
  details: [
10932
11056
  `installed: ${installed}`,
10933
- `available: ${available} (${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL})`,
11057
+ `available: ${available} (${BMAD_NPM_PACKAGE}@${channel})`,
10934
11058
  stable ? `stable latest: ${stable}` : "",
10935
11059
  "run `pj migrate bmad.version` to take the prerelease"
10936
11060
  ].filter(Boolean),
@@ -10944,7 +11068,7 @@ function createBmadChecks() {
10944
11068
  summary: pinnedMismatch ? `BMAD ${installed} does not match ${targetLabel}` : `BMAD ${installed} is behind stable ${stable} \u2014 upgrade available`,
10945
11069
  details: [
10946
11070
  `installed: ${installed}`,
10947
- pinned ? `required transaction pin: ${available}` : `available: ${available} (${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL})`,
11071
+ pinned ? `required transaction pin: ${available}` : `available: ${available} (${BMAD_NPM_PACKAGE}@${channel})`,
10948
11072
  !pinned && stable ? `stable latest: ${stable}` : "",
10949
11073
  "run `pj migrate bmad.version` to upgrade"
10950
11074
  ].filter(Boolean),
@@ -10957,13 +11081,13 @@ function createBmadChecks() {
10957
11081
  }
10958
11082
  {
10959
11083
  const current = readInstalledBmadVersion(ctx.repoRoot);
10960
- const target = ctx.bmadVersionPin?.trim() ?? resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
11084
+ const target = ctx.bmadVersionPin?.trim() ?? bmadChannelTarget(resolveBmadDistTags(ctx.homeDir)?.distTags)?.version;
10961
11085
  if (!current || !target || compareBmadVersions(current, target) === 0) {
10962
11086
  return { id: finding2.id, title: finding2.title, status: "noop", summary: "BMAD already current", changedFiles: [], details: [] };
10963
11087
  }
10964
11088
  }
10965
11089
  const installed = readInstalledBmadVersion(ctx.repoRoot);
10966
- const available = ctx.bmadVersionPin?.trim() ?? resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
11090
+ const available = ctx.bmadVersionPin?.trim() ?? bmadChannelTarget(resolveBmadDistTags(ctx.homeDir)?.distTags)?.version;
10967
11091
  const manifestPath = join5(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
10968
11092
  const manifestSelection = manifestBmadModules(ctx.repoRoot);
10969
11093
  if (manifestSelection.status === "invalid") {
@@ -17586,18 +17710,18 @@ function reportBoardDelivery(board, slug, skipRequested) {
17586
17710
  log("");
17587
17711
  return skipRequested;
17588
17712
  }
17589
- function projectInitActionLabel(kind) {
17590
- switch (kind) {
17713
+ function projectInitActionLabel(action) {
17714
+ switch (action.kind) {
17591
17715
  case "registry.upsert":
17592
17716
  return "Refresh the project registry index";
17593
17717
  case "copier.copy.commonproject":
17594
- return "Render CommonProject scaffold";
17718
+ return action.data.project_type && action.data.project_type !== "base" ? `Render CommonProject scaffold (${action.data.project_type})` : "Render CommonProject scaffold";
17595
17719
  case "project.write-manifest":
17596
17720
  return "Write authoritative repo-local .project.json";
17597
17721
  case "ticket-provider.create-or-link":
17598
17722
  return "Create/link ticket provider project";
17599
17723
  default:
17600
- return kind;
17724
+ return action.kind;
17601
17725
  }
17602
17726
  }
17603
17727
  function registryNeedsUpsert(plan) {
@@ -17622,7 +17746,7 @@ function actionNeedsRun(plan, kind, syncMode) {
17622
17746
  async function selectProjectInitOperations(input) {
17623
17747
  const planOperations = input.plan.actions.filter((action) => actionNeedsRun(input.plan, action.kind, input.syncMode)).map((action) => ({
17624
17748
  value: action.kind,
17625
- label: projectInitActionLabel(action.kind),
17749
+ label: projectInitActionLabel(action),
17626
17750
  hint: action.kind === "registry.upsert" ? input.plan.registryPath : action.kind
17627
17751
  }));
17628
17752
  const parityOperations = input.auditRules.filter((rule) => rule.fixable && rule.status !== "pass" && rule.status !== "skip").map((rule) => ({
@@ -17742,7 +17866,7 @@ program.configureOutput({
17742
17866
  });
17743
17867
  registerNotebookCli(program);
17744
17868
  program.name("pjangler").description("Project subsystem bootstrapper CLI").version(PJANGLER_VERSION);
17745
- program.command("init").argument("[name]", "Name of a NEW project; creates ./<name>. Omit to adopt the repo you are standing in.").description("Initialize the project manifest, scaffold and registry index").option("--description <text>", "Project description").option("--target-dir <path>", "Adopt an existing repo at this path (the only way to init a directory you are not standing in)").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--apply", "Write the project manifest, render the scaffold and refresh its index").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow host-level external effects (systemd, notebook reconcile). The ticket board is created by default and does not need this.").option("--id <project-id>", "Canonical project ID (case-insensitive)").addOption(new Option("--slug <slug>", "Legacy alias for --id").hideHelp()).option("--identifier <identifier>", "Provider ticket prefix (board metadata)").option("--ticket-provider <type>", "Ticket provider: plane | trello (default: existing binding or plane)").option("--board-id <id>", "Board id (Plane project UUID or Trello board id)").option("--board-url <url>", "Deprecated no-op; board URLs are derived from provider + workspace + board-id").option("--skip-board", "Do not create or link a ticket board (the record stays unlinked, and init says so)").option("--workspace <name>", "Ticket workspace/org (Plane workspace; blank for Trello)").option("--registry <location>", `Registry service URL or fixture path (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry, re-rendering files, and renaming an already-registered project").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action(async (name, options) => {
17869
+ program.command("init").argument("[name]", "Name of a NEW project; creates ./<name>. Omit to adopt the repo you are standing in.").description("Initialize the project manifest, scaffold and registry index").option("--description <text>", "Project description").option("--target-dir <path>", "Adopt an existing repo at this path (the only way to init a directory you are not standing in)").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language recorded for the project (default: the recorded one, else the project type's, else python)").option("--obsidian-plugin", "Render the CommonProject variant built on the canonical Obsidian sample plugin (new projects; on an existing repo, records one that already is a plugin)").option("--apply", "Write the project manifest, render the scaffold and refresh its index").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow host-level external effects (systemd, notebook reconcile). The ticket board is created by default and does not need this.").option("--id <project-id>", "Canonical project ID (case-insensitive)").addOption(new Option("--slug <slug>", "Legacy alias for --id").hideHelp()).option("--identifier <identifier>", "Provider ticket prefix (board metadata)").option("--ticket-provider <type>", "Ticket provider: plane | trello (default: existing binding or plane)").option("--board-id <id>", "Board id (Plane project UUID or Trello board id)").option("--board-url <url>", "Deprecated no-op; board URLs are derived from provider + workspace + board-id").option("--skip-board", "Do not create or link a ticket board (the record stays unlinked, and init says so)").option("--workspace <name>", "Ticket workspace/org (Plane workspace; blank for Trello)").option("--registry <location>", `Registry service URL or fixture path (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry, re-rendering files, and renaming an already-registered project").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action(async (name, options) => {
17746
17870
  if (name && getRecipeNames().includes(name)) {
17747
17871
  if (!options.json) {
17748
17872
  console.error(`${yellow(glyph.warn)} ${dim(`"pjangler init ${name}" is deprecated \u2014 use "pjangler add ${name}". Forwarding\u2026`)}`);
@@ -17865,7 +17989,7 @@ boardCmd.command("modules").description("List this project's modules").option("-
17865
17989
  }
17866
17990
  });
17867
17991
  var projectCmd = program.command("project", { hidden: true }).description("Legacy aliases for project commands");
17868
- projectCmd.command("init").argument("[name]", "Project display name").description("Plan or apply a registry-backed CommonProject initialization or legacy repo sync").option("--description <text>", "Project description").option("--target-dir <path>", "Adopt an existing repo at this path (the only way to init a directory you are not standing in)").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--apply", "Write the project manifest, render the scaffold and refresh its index").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow host-level external effects (systemd, notebook reconcile). The ticket board is created by default and does not need this.").option("--id <project-id>", "Canonical project ID (case-insensitive)").addOption(new Option("--slug <slug>", "Legacy alias for --id").hideHelp()).option("--identifier <identifier>", "Provider ticket prefix (board metadata)").option("--ticket-provider <type>", "Ticket provider: plane | trello (default: existing binding or plane)").option("--board-id <id>", "Board id (Plane project UUID or Trello board id)").option("--board-url <url>", "Deprecated no-op; board URLs are derived from provider + workspace + board-id").option("--skip-board", "Do not create or link a ticket board (the record stays unlinked, and init says so)").option("--workspace <name>", "Ticket workspace/org (Plane workspace; blank for Trello)").option("--registry <location>", `Registry service URL or fixture path (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry, re-rendering files, and renaming an already-registered project").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action((name, options) => {
17992
+ projectCmd.command("init").argument("[name]", "Project display name").description("Plan or apply a registry-backed CommonProject initialization or legacy repo sync").option("--description <text>", "Project description").option("--target-dir <path>", "Adopt an existing repo at this path (the only way to init a directory you are not standing in)").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language recorded for the project (default: the recorded one, else the project type's, else python)").option("--obsidian-plugin", "Render the CommonProject variant built on the canonical Obsidian sample plugin (new projects; on an existing repo, records one that already is a plugin)").option("--apply", "Write the project manifest, render the scaffold and refresh its index").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow host-level external effects (systemd, notebook reconcile). The ticket board is created by default and does not need this.").option("--id <project-id>", "Canonical project ID (case-insensitive)").addOption(new Option("--slug <slug>", "Legacy alias for --id").hideHelp()).option("--identifier <identifier>", "Provider ticket prefix (board metadata)").option("--ticket-provider <type>", "Ticket provider: plane | trello (default: existing binding or plane)").option("--board-id <id>", "Board id (Plane project UUID or Trello board id)").option("--board-url <url>", "Deprecated no-op; board URLs are derived from provider + workspace + board-id").option("--skip-board", "Do not create or link a ticket board (the record stays unlinked, and init says so)").option("--workspace <name>", "Ticket workspace/org (Plane workspace; blank for Trello)").option("--registry <location>", `Registry service URL or fixture path (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry, re-rendering files, and renaming an already-registered project").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action((name, options) => {
17869
17993
  if (!options.json) console.error(`${yellow(glyph.warn)} ${dim('"pjangler project init" is deprecated \u2014 use "pjangler init".')}`);
17870
17994
  return runProjectInit(name, options);
17871
17995
  });
@@ -17880,6 +18004,7 @@ async function runProjectInit(name, options) {
17880
18004
  targetDir: target.targetDir,
17881
18005
  sourceSkill: options.sourceSkill,
17882
18006
  primaryLanguage: options.primaryLanguage,
18007
+ projectType: options.obsidianPlugin ? "obsidian-plugin" : void 0,
17883
18008
  apply,
17884
18009
  live: options.live ?? false,
17885
18010
  projectSlug: target.slug,
@@ -17968,6 +18093,14 @@ async function runProjectInit(name, options) {
17968
18093
  if (result2.ok && result2.changedFiles.length) console.log(` ${green(glyph.pass)} ${bold("Project synchronized")} ${dim(glyph.dot)} ${cyan(plan.project.slug)}`);
17969
18094
  if (result2.ok && result2.changedFiles.length === 0) console.log(` ${green(glyph.pass)} ${dim("Already in parity")} ${dim(glyph.dot)} ${cyan(plan.project.slug)}`);
17970
18095
  reportBoardDelivery(board, plan.project.slug, skipRequested);
18096
+ const rendered = selectedPlan.actions.find((action) => action.kind === "copier.copy.commonproject");
18097
+ if (result2.ok && rendered?.kind === "copier.copy.commonproject" && rendered.data.project_type === "obsidian-plugin") {
18098
+ console.log(` ${bold("Next")} ${dim(`cd ${target.targetDir}`)}`);
18099
+ console.log(` ${cyan("npm install")} ${dim("dev dependencies, pinned by the committed package-lock.json")}`);
18100
+ console.log(` ${cyan("npm run dev")} ${dim("watch build -> main.js")}`);
18101
+ console.log(` ${cyan("mise run plugin:link <vault>")} ${dim("load it in an Obsidian vault")}`);
18102
+ console.log("");
18103
+ }
17971
18104
  }
17972
18105
  process.exitCode = result2.ok && boardDelivered ? 0 : 1;
17973
18106
  } catch (err) {