@delorenj/pjangler 1.5.11 → 1.5.12
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/.mise/scripts/versioning.sh +26 -5
- package/README.md +25 -0
- package/dist/index.js +150 -27
- package/dist/mcp-server.js +134 -20
- package/dist/notebook/journal-service-main.js +4 -1
- package/dist/project-registry-service.js +1 -1
- package/package.json +1 -1
- package/templates/commonproject/copier.yml +102 -1
- package/templates/commonproject/template/.github/workflows/{% if is_obsidian_plugin %}lint.yml{% endif %} +29 -0
- package/templates/commonproject/template/.github/workflows/{% if is_obsidian_plugin %}release.yml{% endif %} +62 -0
- package/templates/commonproject/template/.mise/{% if is_obsidian_plugin %}tasks{% endif %}/plugin/link +27 -0
- package/templates/commonproject/template/AGENTS.md.jinja +67 -0
- package/templates/commonproject/template/{% if is_obsidian_plugin %}.editorconfig{% endif %} +15 -0
- package/templates/commonproject/template/{% if is_obsidian_plugin %}.npmrc{% endif %} +1 -0
- package/templates/commonproject/template/{% if is_obsidian_plugin %}esbuild.config.mjs{% endif %} +49 -0
- package/templates/commonproject/template/{% if is_obsidian_plugin %}eslint.config.mts{% endif %} +38 -0
- package/templates/commonproject/template/{% if is_obsidian_plugin %}manifest.json{% endif %}.jinja +9 -0
- package/templates/commonproject/template/{% if is_obsidian_plugin %}package-lock.json{% endif %}.jinja +5053 -0
- package/templates/commonproject/template/{% if is_obsidian_plugin %}package.json{% endif %}.jinja +28 -0
- package/templates/commonproject/template/{% if is_obsidian_plugin %}src{% endif %}/main.ts.jinja +105 -0
- package/templates/commonproject/template/{% if is_obsidian_plugin %}src{% endif %}/settings.ts.jinja +38 -0
- package/templates/commonproject/template/{% if is_obsidian_plugin %}styles.css{% endif %} +8 -0
- package/templates/commonproject/template/{% if is_obsidian_plugin %}tsconfig.json{% endif %} +19 -0
- package/templates/commonproject/template/{% if is_obsidian_plugin %}version-bump.mjs{% endif %} +17 -0
- 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"
|
|
133
|
+
local type="$1" path="$2" new="$3"
|
|
123
134
|
case "$type" in
|
|
124
135
|
json)
|
|
125
136
|
[[ -f "$path" ]] || return 0
|
|
126
|
-
|
|
127
|
-
|
|
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:
|
|
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")
|
|
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];
|
|
@@ -10068,6 +10162,15 @@ function createMiseChecks() {
|
|
|
10068
10162
|
if (!text2?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
10069
10163
|
if (!existsSync3(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
10070
10164
|
if (!existsSync3(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
10165
|
+
const conf = safeReadText2(manifestPath);
|
|
10166
|
+
const script = safeReadText2(versioningPath);
|
|
10167
|
+
if (conf !== null && script !== null) {
|
|
10168
|
+
const unsupported = unsupportedVersionTypes(conf, script);
|
|
10169
|
+
if (unsupported.length) details.push(`.mise/scripts/versioning.sh cannot version type(s) ${unsupported.join(", ")} named in .mise/version-files.conf`);
|
|
10170
|
+
}
|
|
10171
|
+
if (conf !== null && isObsidianPluginRepo(ctx, ctx.repoRoot) && !/^obsidian\s+manifest\.json\s*$/m.test(conf)) {
|
|
10172
|
+
details.push(".mise/version-files.conf does not version manifest.json/versions.json for this obsidian-plugin project");
|
|
10173
|
+
}
|
|
10071
10174
|
return {
|
|
10072
10175
|
id: "mise.versioning",
|
|
10073
10176
|
title: "managed mise versioning block",
|
|
@@ -10609,6 +10712,15 @@ function strayOpInjectLines(text2) {
|
|
|
10609
10712
|
}
|
|
10610
10713
|
return [...new Set(lines)].sort((a, b) => a - b);
|
|
10611
10714
|
}
|
|
10715
|
+
var COPIER_ANSWERS_HEADER = "# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY";
|
|
10716
|
+
function parseCopierAnswers(text2) {
|
|
10717
|
+
try {
|
|
10718
|
+
const parsed = YAML2.parse(text2);
|
|
10719
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { answers: parsed } : { problem: ".copier-answers.yml is not a YAML mapping" };
|
|
10720
|
+
} catch (error) {
|
|
10721
|
+
return { problem: `.copier-answers.yml is not valid YAML (${errorText(error).split("\n")[0]})` };
|
|
10722
|
+
}
|
|
10723
|
+
}
|
|
10612
10724
|
function createProjectProvenanceChecks() {
|
|
10613
10725
|
return [
|
|
10614
10726
|
{
|
|
@@ -10622,16 +10734,14 @@ function createProjectProvenanceChecks() {
|
|
|
10622
10734
|
if (!text2) {
|
|
10623
10735
|
details.push(".copier-answers.yml missing");
|
|
10624
10736
|
} else {
|
|
10625
|
-
if (!text2.startsWith(
|
|
10626
|
-
|
|
10627
|
-
if (
|
|
10628
|
-
|
|
10629
|
-
|
|
10630
|
-
|
|
10631
|
-
|
|
10632
|
-
|
|
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");
|
|
10737
|
+
if (!text2.startsWith(COPIER_ANSWERS_HEADER)) details.push("missing Copier overwrite warning header");
|
|
10738
|
+
const parsed = parseCopierAnswers(text2);
|
|
10739
|
+
if ("problem" in parsed) details.push(parsed.problem);
|
|
10740
|
+
else {
|
|
10741
|
+
const { answers } = parsed;
|
|
10742
|
+
if (answers._src_path === void 0) details.push("_src_path missing");
|
|
10743
|
+
if (project?.project_name && String(answers.project_name ?? "") !== String(project.project_name)) details.push("project_name drift between .copier-answers.yml and .project.json");
|
|
10744
|
+
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
10745
|
}
|
|
10636
10746
|
}
|
|
10637
10747
|
return {
|
|
@@ -10646,14 +10756,18 @@ function createProjectProvenanceChecks() {
|
|
|
10646
10756
|
migrate: (ctx, finding2) => {
|
|
10647
10757
|
const changedFiles = [];
|
|
10648
10758
|
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
10759
|
const path = join5(ctx.repoRoot, ".copier-answers.yml");
|
|
10656
|
-
|
|
10760
|
+
const current = safeReadText2(path);
|
|
10761
|
+
const existing = current ? YAML2.parseDocument(current) : void 0;
|
|
10762
|
+
const document = existing && !existing.errors.length && YAML2.isMap(existing.contents) ? existing : new YAML2.Document({});
|
|
10763
|
+
document.set("_src_path", join5(ctx.pjanglerRoot, "templates", "commonproject"));
|
|
10764
|
+
document.set("project_description", String(project.project_description));
|
|
10765
|
+
document.set("project_name", String(project.project_name));
|
|
10766
|
+
document.set("ticket_provider", String(project.ticket_provider?.type ?? "plane"));
|
|
10767
|
+
const body = String(document);
|
|
10768
|
+
const text2 = body.startsWith(COPIER_ANSWERS_HEADER) ? body : `${COPIER_ANSWERS_HEADER}
|
|
10769
|
+
${body}`;
|
|
10770
|
+
if (current !== text2) {
|
|
10657
10771
|
changedFiles.push(path);
|
|
10658
10772
|
if (!ctx.dryRun) writeText(path, text2);
|
|
10659
10773
|
}
|
|
@@ -17586,18 +17700,18 @@ function reportBoardDelivery(board, slug, skipRequested) {
|
|
|
17586
17700
|
log("");
|
|
17587
17701
|
return skipRequested;
|
|
17588
17702
|
}
|
|
17589
|
-
function projectInitActionLabel(
|
|
17590
|
-
switch (kind) {
|
|
17703
|
+
function projectInitActionLabel(action) {
|
|
17704
|
+
switch (action.kind) {
|
|
17591
17705
|
case "registry.upsert":
|
|
17592
17706
|
return "Refresh the project registry index";
|
|
17593
17707
|
case "copier.copy.commonproject":
|
|
17594
|
-
return "Render CommonProject scaffold";
|
|
17708
|
+
return action.data.project_type && action.data.project_type !== "base" ? `Render CommonProject scaffold (${action.data.project_type})` : "Render CommonProject scaffold";
|
|
17595
17709
|
case "project.write-manifest":
|
|
17596
17710
|
return "Write authoritative repo-local .project.json";
|
|
17597
17711
|
case "ticket-provider.create-or-link":
|
|
17598
17712
|
return "Create/link ticket provider project";
|
|
17599
17713
|
default:
|
|
17600
|
-
return kind;
|
|
17714
|
+
return action.kind;
|
|
17601
17715
|
}
|
|
17602
17716
|
}
|
|
17603
17717
|
function registryNeedsUpsert(plan) {
|
|
@@ -17622,7 +17736,7 @@ function actionNeedsRun(plan, kind, syncMode) {
|
|
|
17622
17736
|
async function selectProjectInitOperations(input) {
|
|
17623
17737
|
const planOperations = input.plan.actions.filter((action) => actionNeedsRun(input.plan, action.kind, input.syncMode)).map((action) => ({
|
|
17624
17738
|
value: action.kind,
|
|
17625
|
-
label: projectInitActionLabel(action
|
|
17739
|
+
label: projectInitActionLabel(action),
|
|
17626
17740
|
hint: action.kind === "registry.upsert" ? input.plan.registryPath : action.kind
|
|
17627
17741
|
}));
|
|
17628
17742
|
const parityOperations = input.auditRules.filter((rule) => rule.fixable && rule.status !== "pass" && rule.status !== "skip").map((rule) => ({
|
|
@@ -17742,7 +17856,7 @@ program.configureOutput({
|
|
|
17742
17856
|
});
|
|
17743
17857
|
registerNotebookCli(program);
|
|
17744
17858
|
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
|
|
17859
|
+
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
17860
|
if (name && getRecipeNames().includes(name)) {
|
|
17747
17861
|
if (!options.json) {
|
|
17748
17862
|
console.error(`${yellow(glyph.warn)} ${dim(`"pjangler init ${name}" is deprecated \u2014 use "pjangler add ${name}". Forwarding\u2026`)}`);
|
|
@@ -17865,7 +17979,7 @@ boardCmd.command("modules").description("List this project's modules").option("-
|
|
|
17865
17979
|
}
|
|
17866
17980
|
});
|
|
17867
17981
|
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
|
|
17982
|
+
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
17983
|
if (!options.json) console.error(`${yellow(glyph.warn)} ${dim('"pjangler project init" is deprecated \u2014 use "pjangler init".')}`);
|
|
17870
17984
|
return runProjectInit(name, options);
|
|
17871
17985
|
});
|
|
@@ -17880,6 +17994,7 @@ async function runProjectInit(name, options) {
|
|
|
17880
17994
|
targetDir: target.targetDir,
|
|
17881
17995
|
sourceSkill: options.sourceSkill,
|
|
17882
17996
|
primaryLanguage: options.primaryLanguage,
|
|
17997
|
+
projectType: options.obsidianPlugin ? "obsidian-plugin" : void 0,
|
|
17883
17998
|
apply,
|
|
17884
17999
|
live: options.live ?? false,
|
|
17885
18000
|
projectSlug: target.slug,
|
|
@@ -17968,6 +18083,14 @@ async function runProjectInit(name, options) {
|
|
|
17968
18083
|
if (result2.ok && result2.changedFiles.length) console.log(` ${green(glyph.pass)} ${bold("Project synchronized")} ${dim(glyph.dot)} ${cyan(plan.project.slug)}`);
|
|
17969
18084
|
if (result2.ok && result2.changedFiles.length === 0) console.log(` ${green(glyph.pass)} ${dim("Already in parity")} ${dim(glyph.dot)} ${cyan(plan.project.slug)}`);
|
|
17970
18085
|
reportBoardDelivery(board, plan.project.slug, skipRequested);
|
|
18086
|
+
const rendered = selectedPlan.actions.find((action) => action.kind === "copier.copy.commonproject");
|
|
18087
|
+
if (result2.ok && rendered?.kind === "copier.copy.commonproject" && rendered.data.project_type === "obsidian-plugin") {
|
|
18088
|
+
console.log(` ${bold("Next")} ${dim(`cd ${target.targetDir}`)}`);
|
|
18089
|
+
console.log(` ${cyan("npm install")} ${dim("dev dependencies, pinned by the committed package-lock.json")}`);
|
|
18090
|
+
console.log(` ${cyan("npm run dev")} ${dim("watch build -> main.js")}`);
|
|
18091
|
+
console.log(` ${cyan("mise run plugin:link <vault>")} ${dim("load it in an Obsidian vault")}`);
|
|
18092
|
+
console.log("");
|
|
18093
|
+
}
|
|
17971
18094
|
}
|
|
17972
18095
|
process.exitCode = result2.ok && boardDelivered ? 0 : 1;
|
|
17973
18096
|
} catch (err) {
|