@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
package/dist/mcp-server.js
CHANGED
|
@@ -828,6 +828,38 @@ import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
|
|
|
828
828
|
import { basename as basename6, delimiter as delimiter2, dirname as dirname10, isAbsolute as isAbsolute5, join as join11, relative as relative7, resolve as resolve9, sep as sep2, win32 } from "node:path";
|
|
829
829
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
830
830
|
import YAML4 from "yaml";
|
|
831
|
+
function normalizeProjectType(value, label = "Project type") {
|
|
832
|
+
const text = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
833
|
+
if (PROJECT_TYPES.includes(text)) return text;
|
|
834
|
+
throw new Error(`${label} ${JSON.stringify(value)} is not one of: ${PROJECT_TYPES.join(", ")}`);
|
|
835
|
+
}
|
|
836
|
+
function looksLikeObsidianPlugin(dir) {
|
|
837
|
+
try {
|
|
838
|
+
const manifest = JSON.parse(readFileSync8(join11(dir, "manifest.json"), "utf8"));
|
|
839
|
+
return isRecord2(manifest) && typeof manifest.id === "string" && typeof manifest.minAppVersion === "string";
|
|
840
|
+
} catch {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
function assertTemplateOffersProjectType(pjanglerRoot, projectType) {
|
|
845
|
+
if (projectType === "base") return;
|
|
846
|
+
const copierYml = join11(pjanglerRoot, "templates", "commonproject", "copier.yml");
|
|
847
|
+
let choices;
|
|
848
|
+
try {
|
|
849
|
+
const parsed = YAML4.parse(readFileSync8(copierYml, "utf8"));
|
|
850
|
+
choices = isRecord2(parsed) && isRecord2(parsed.project_type) ? parsed.project_type.choices : void 0;
|
|
851
|
+
} catch {
|
|
852
|
+
choices = void 0;
|
|
853
|
+
}
|
|
854
|
+
const offered = isRecord2(choices) ? Object.values(choices) : Array.isArray(choices) ? choices : [];
|
|
855
|
+
if (!offered.includes(projectType)) {
|
|
856
|
+
throw new Error(`The CommonProject template at ${dirname10(copierYml)} cannot render project type "${projectType}" (its copier.yml offers no such project_type); update pjangler or its templates/commonproject pin`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function operatorDisplayName(cwd) {
|
|
860
|
+
const result2 = spawnSync4("git", ["config", "--get", "user.name"], { cwd: existsSync7(cwd) ? cwd : void 0, encoding: "utf8", timeout: 5e3 });
|
|
861
|
+
return result2.status === 0 ? result2.stdout.trim() : "";
|
|
862
|
+
}
|
|
831
863
|
function providerAssignsIdentifiers(type) {
|
|
832
864
|
return IDENTIFIER_ASSIGNING_PROVIDERS.includes((type ?? "").trim());
|
|
833
865
|
}
|
|
@@ -1431,6 +1463,26 @@ function planProjectInit(input) {
|
|
|
1431
1463
|
const overwrite = input.overwrite ?? input.force ?? false;
|
|
1432
1464
|
const agents = createSafeRecord(Object.entries(existing?.agents ?? {}));
|
|
1433
1465
|
const scaffold = input.scaffold ?? true;
|
|
1466
|
+
const recordedTypeValue = existing?.template?.commonproject?.project_type;
|
|
1467
|
+
const recordedType = recordedTypeValue === void 0 ? void 0 : normalizeProjectType(recordedTypeValue, `Recorded project type of ${slug}`);
|
|
1468
|
+
const requestedType = input.projectType === void 0 ? void 0 : normalizeProjectType(input.projectType);
|
|
1469
|
+
const freshRender = scaffold && localManifest === void 0 && !existsSync7(join11(targetDir, ".copier-answers.yml"));
|
|
1470
|
+
if (requestedType && !freshRender && requestedType !== (recordedType ?? "base")) {
|
|
1471
|
+
const adoptingPlugin = !scaffold && recordedType === void 0 && requestedType === "obsidian-plugin" && looksLikeObsidianPlugin(targetDir);
|
|
1472
|
+
if (!adoptingPlugin) {
|
|
1473
|
+
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)`);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
const projectType = requestedType ?? recordedType ?? "base";
|
|
1477
|
+
const typeLanguages = PROJECT_TYPE_LANGUAGES[projectType];
|
|
1478
|
+
if (input.primaryLanguage !== void 0 && typeLanguages && !typeLanguages.includes(input.primaryLanguage)) {
|
|
1479
|
+
throw new Error(`A ${projectType} project is written in ${typeLanguages.join(" or ")}, not ${input.primaryLanguage}`);
|
|
1480
|
+
}
|
|
1481
|
+
const recordedLanguageValue = (localManifest ? localManifest.template : indexed?.template)?.commonproject?.primary_language;
|
|
1482
|
+
const recordedLanguage = typeof recordedLanguageValue === "string" && recordedLanguageValue ? recordedLanguageValue : void 0;
|
|
1483
|
+
const newlyTyped = requestedType !== void 0 && requestedType !== recordedType;
|
|
1484
|
+
const recordedLanguageFits = !typeLanguages || recordedLanguage !== void 0 && typeLanguages.includes(recordedLanguage);
|
|
1485
|
+
const primaryLanguage = input.primaryLanguage ?? (newlyTyped && !recordedLanguageFits ? typeLanguages?.[0] : void 0) ?? recordedLanguage ?? typeLanguages?.[0] ?? "python";
|
|
1434
1486
|
const candidateProject = {
|
|
1435
1487
|
...existing ?? {},
|
|
1436
1488
|
name: input.name,
|
|
@@ -1452,7 +1504,10 @@ function planProjectInit(input) {
|
|
|
1452
1504
|
commonproject: {
|
|
1453
1505
|
...existing?.template?.commonproject,
|
|
1454
1506
|
enabled: true,
|
|
1455
|
-
primary_language:
|
|
1507
|
+
primary_language: primaryLanguage,
|
|
1508
|
+
// Written only for a non-base type (or one already recorded), so a
|
|
1509
|
+
// base record never gains a key and stays equivalent to its last run.
|
|
1510
|
+
...projectType !== "base" || recordedType !== void 0 ? { project_type: projectType } : {}
|
|
1456
1511
|
}
|
|
1457
1512
|
},
|
|
1458
1513
|
ticket_provider: { ...existing?.ticket_provider, ...buildTicketProviderBlock({
|
|
@@ -1488,6 +1543,7 @@ function planProjectInit(input) {
|
|
|
1488
1543
|
{ kind: "registry.upsert", registryPath, slug, project }
|
|
1489
1544
|
];
|
|
1490
1545
|
if (scaffold) {
|
|
1546
|
+
assertTemplateOffersProjectType(pjanglerRoot, projectType);
|
|
1491
1547
|
actions.push(buildCommonProjectCopierAction({
|
|
1492
1548
|
pjanglerRoot,
|
|
1493
1549
|
targetDir,
|
|
@@ -1501,6 +1557,8 @@ function planProjectInit(input) {
|
|
|
1501
1557
|
boardId: project.ticket_provider.board_id ?? "",
|
|
1502
1558
|
projectIdentifier: identifier,
|
|
1503
1559
|
primaryLanguage: project.template.commonproject.primary_language,
|
|
1560
|
+
projectType,
|
|
1561
|
+
...projectType === "obsidian-plugin" ? { pluginAuthor: operatorDisplayName(input.cwd ?? process.cwd()) } : {},
|
|
1504
1562
|
agentHooksLayer: resolveAgentHooksLayer2(input.agentHooksLayer),
|
|
1505
1563
|
overwrite
|
|
1506
1564
|
}));
|
|
@@ -1639,6 +1697,13 @@ async function executeProjectInitPlan(plan, options = {}) {
|
|
|
1639
1697
|
errors.push(`copier exited with status ${result2.status ?? "unknown"}`);
|
|
1640
1698
|
break;
|
|
1641
1699
|
}
|
|
1700
|
+
const projectType = action.data.project_type ?? "base";
|
|
1701
|
+
const missing = (PROJECT_TYPE_SENTINELS[projectType] ?? []).filter((rel) => !existsSync7(join11(action.targetDir, rel)));
|
|
1702
|
+
if (missing.length) {
|
|
1703
|
+
errors.push(`copier rendered no ${projectType} project: ${missing.join(", ")} missing from ${action.targetDir}`);
|
|
1704
|
+
break;
|
|
1705
|
+
}
|
|
1706
|
+
if (projectType !== "base") logs.push(`commonproject: rendered as ${projectType}`);
|
|
1642
1707
|
} else if (action.kind === "project.write-manifest") {
|
|
1643
1708
|
if (action.expectedContent !== void 0 && (!existsSync7(action.path) || readFileSync8(action.path, "utf8") !== action.expectedContent)) {
|
|
1644
1709
|
errors.push(`Manifest changed after planning: ${action.path}; re-plan before applying`);
|
|
@@ -1771,6 +1836,8 @@ function buildCommonProjectCopierAction(input) {
|
|
|
1771
1836
|
board_id: input.boardId ?? input.planeProjectId ?? "",
|
|
1772
1837
|
project_identifier: input.projectIdentifier,
|
|
1773
1838
|
primary_language: input.primaryLanguage,
|
|
1839
|
+
project_type: input.projectType ?? "base",
|
|
1840
|
+
...input.pluginAuthor !== void 0 ? { plugin_author: input.pluginAuthor } : {},
|
|
1774
1841
|
agent_hooks_layer: input.agentHooksLayer ?? true ? "true" : "false"
|
|
1775
1842
|
};
|
|
1776
1843
|
const command = ["copier", "copy", "--trust", "--vcs-ref=HEAD", templateDir, input.targetDir, "--defaults"];
|
|
@@ -1871,7 +1938,7 @@ function expandHome(path) {
|
|
|
1871
1938
|
function isRecord2(value) {
|
|
1872
1939
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1873
1940
|
}
|
|
1874
|
-
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;
|
|
1941
|
+
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;
|
|
1875
1942
|
var init_project = __esm({
|
|
1876
1943
|
"src/project/index.ts"() {
|
|
1877
1944
|
"use strict";
|
|
@@ -1887,6 +1954,13 @@ var init_project = __esm({
|
|
|
1887
1954
|
TICKET_PROVIDER_ADAPTERS_ENV = "PJ_TICKET_PROVIDER_ADAPTERS";
|
|
1888
1955
|
PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
1889
1956
|
DEFAULT_NEW_PROJECT_STATUS = "active";
|
|
1957
|
+
PROJECT_TYPES = ["base", "obsidian-plugin"];
|
|
1958
|
+
PROJECT_TYPE_LANGUAGES = {
|
|
1959
|
+
"obsidian-plugin": ["typescript", "javascript"]
|
|
1960
|
+
};
|
|
1961
|
+
PROJECT_TYPE_SENTINELS = {
|
|
1962
|
+
"obsidian-plugin": ["manifest.json", join11("src", "main.ts")]
|
|
1963
|
+
};
|
|
1890
1964
|
BOARD_URL_DEPRECATION_WARNING = "boardUrl is deprecated and ignored; board URLs are derived at runtime and are never persisted.";
|
|
1891
1965
|
PROJECT_IDENTIFIER_SOURCES = ["provider", "proposed"];
|
|
1892
1966
|
IDENTIFIER_ASSIGNING_PROVIDERS = ["plane", "linear"];
|
|
@@ -7678,9 +7752,24 @@ function lstatIfPresent(path) {
|
|
|
7678
7752
|
}
|
|
7679
7753
|
}
|
|
7680
7754
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
7755
|
+
if (isObsidianPluginRepo(ctx, repoRoot)) return OBSIDIAN_VERSION_FILES_CONF;
|
|
7681
7756
|
const packageJson = join5(repoRoot, "package.json");
|
|
7682
7757
|
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";
|
|
7683
7758
|
}
|
|
7759
|
+
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";
|
|
7760
|
+
function recordedProjectType(ctx) {
|
|
7761
|
+
return record(record(readProjectJson(ctx)?.template).commonproject).project_type;
|
|
7762
|
+
}
|
|
7763
|
+
function isObsidianPluginRepo(ctx, repoRoot) {
|
|
7764
|
+
const recorded = recordedProjectType(ctx);
|
|
7765
|
+
if (recorded !== void 0) return recorded === "obsidian-plugin";
|
|
7766
|
+
const manifest = record(tryParseJson(safeReadText2(join5(repoRoot, "manifest.json"))));
|
|
7767
|
+
return typeof manifest.id === "string" && typeof manifest.minAppVersion === "string";
|
|
7768
|
+
}
|
|
7769
|
+
function unsupportedVersionTypes(conf, script) {
|
|
7770
|
+
const types = conf.split("\n").map((line) => line.trim().split(/\s+/)[0] ?? "").filter((type) => type && !type.startsWith("#"));
|
|
7771
|
+
return [...new Set(types)].filter((type) => !new RegExp(`(^|[\\s|])${type.replace(/[^a-z0-9-]/gi, "")}(\\|[a-z-]+)*\\)`, "m").test(script));
|
|
7772
|
+
}
|
|
7684
7773
|
var BASE_MISE_PATH_ENTRIES = [".mise/scripts", "agents/hermes/pm"];
|
|
7685
7774
|
function requiredMisePathEntries(_ctx) {
|
|
7686
7775
|
return [...BASE_MISE_PATH_ENTRIES];
|
|
@@ -8884,6 +8973,15 @@ function createMiseChecks() {
|
|
|
8884
8973
|
if (!text?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
8885
8974
|
if (!existsSync3(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
8886
8975
|
if (!existsSync3(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
8976
|
+
const conf = safeReadText2(manifestPath);
|
|
8977
|
+
const script = safeReadText2(versioningPath);
|
|
8978
|
+
if (conf !== null && script !== null) {
|
|
8979
|
+
const unsupported = unsupportedVersionTypes(conf, script);
|
|
8980
|
+
if (unsupported.length) details.push(`.mise/scripts/versioning.sh cannot version type(s) ${unsupported.join(", ")} named in .mise/version-files.conf`);
|
|
8981
|
+
}
|
|
8982
|
+
if (conf !== null && isObsidianPluginRepo(ctx, ctx.repoRoot) && !/^obsidian\s+manifest\.json\s*$/m.test(conf)) {
|
|
8983
|
+
details.push(".mise/version-files.conf does not version manifest.json/versions.json for this obsidian-plugin project");
|
|
8984
|
+
}
|
|
8887
8985
|
return {
|
|
8888
8986
|
id: "mise.versioning",
|
|
8889
8987
|
title: "managed mise versioning block",
|
|
@@ -9425,6 +9523,15 @@ function strayOpInjectLines(text) {
|
|
|
9425
9523
|
}
|
|
9426
9524
|
return [...new Set(lines)].sort((a, b) => a - b);
|
|
9427
9525
|
}
|
|
9526
|
+
var COPIER_ANSWERS_HEADER = "# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY";
|
|
9527
|
+
function parseCopierAnswers(text) {
|
|
9528
|
+
try {
|
|
9529
|
+
const parsed = YAML2.parse(text);
|
|
9530
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { answers: parsed } : { problem: ".copier-answers.yml is not a YAML mapping" };
|
|
9531
|
+
} catch (error) {
|
|
9532
|
+
return { problem: `.copier-answers.yml is not valid YAML (${errorText(error).split("\n")[0]})` };
|
|
9533
|
+
}
|
|
9534
|
+
}
|
|
9428
9535
|
function createProjectProvenanceChecks() {
|
|
9429
9536
|
return [
|
|
9430
9537
|
{
|
|
@@ -9438,16 +9545,14 @@ function createProjectProvenanceChecks() {
|
|
|
9438
9545
|
if (!text) {
|
|
9439
9546
|
details.push(".copier-answers.yml missing");
|
|
9440
9547
|
} else {
|
|
9441
|
-
if (!text.startsWith(
|
|
9442
|
-
|
|
9443
|
-
if (
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
const yamlDesc = descMatch?.[1]?.replace(/\n\s+/g, " ").trim() ?? "";
|
|
9450
|
-
if (yamlDesc !== String(project.project_description)) details.push("project_description drift between .copier-answers.yml and .project.json");
|
|
9548
|
+
if (!text.startsWith(COPIER_ANSWERS_HEADER)) details.push("missing Copier overwrite warning header");
|
|
9549
|
+
const parsed = parseCopierAnswers(text);
|
|
9550
|
+
if ("problem" in parsed) details.push(parsed.problem);
|
|
9551
|
+
else {
|
|
9552
|
+
const { answers } = parsed;
|
|
9553
|
+
if (answers._src_path === void 0) details.push("_src_path missing");
|
|
9554
|
+
if (project?.project_name && String(answers.project_name ?? "") !== String(project.project_name)) details.push("project_name drift between .copier-answers.yml and .project.json");
|
|
9555
|
+
if (project?.project_description && String(answers.project_description ?? "") !== String(project.project_description)) details.push("project_description drift between .copier-answers.yml and .project.json");
|
|
9451
9556
|
}
|
|
9452
9557
|
}
|
|
9453
9558
|
return {
|
|
@@ -9462,14 +9567,18 @@ function createProjectProvenanceChecks() {
|
|
|
9462
9567
|
migrate: (ctx, finding2) => {
|
|
9463
9568
|
const changedFiles = [];
|
|
9464
9569
|
const project = canonicalProjectJson(ctx);
|
|
9465
|
-
const text = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
9466
|
-
_src_path: ${join5(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
9467
|
-
project_description: ${String(project.project_description)}
|
|
9468
|
-
project_name: ${String(project.project_name)}
|
|
9469
|
-
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
9470
|
-
`;
|
|
9471
9570
|
const path = join5(ctx.repoRoot, ".copier-answers.yml");
|
|
9472
|
-
|
|
9571
|
+
const current = safeReadText2(path);
|
|
9572
|
+
const existing = current ? YAML2.parseDocument(current) : void 0;
|
|
9573
|
+
const document = existing && !existing.errors.length && YAML2.isMap(existing.contents) ? existing : new YAML2.Document({});
|
|
9574
|
+
document.set("_src_path", join5(ctx.pjanglerRoot, "templates", "commonproject"));
|
|
9575
|
+
document.set("project_description", String(project.project_description));
|
|
9576
|
+
document.set("project_name", String(project.project_name));
|
|
9577
|
+
document.set("ticket_provider", String(project.ticket_provider?.type ?? "plane"));
|
|
9578
|
+
const body = String(document);
|
|
9579
|
+
const text = body.startsWith(COPIER_ANSWERS_HEADER) ? body : `${COPIER_ANSWERS_HEADER}
|
|
9580
|
+
${body}`;
|
|
9581
|
+
if (current !== text) {
|
|
9473
9582
|
changedFiles.push(path);
|
|
9474
9583
|
if (!ctx.dryRun) writeText(path, text);
|
|
9475
9584
|
}
|
|
@@ -14427,6 +14536,7 @@ var server = new McpServer({
|
|
|
14427
14536
|
version: PJANGLER_VERSION
|
|
14428
14537
|
});
|
|
14429
14538
|
var TICKET_PROVIDER_SCHEMA = z.enum(["plane", "trello"]);
|
|
14539
|
+
var PROJECT_TYPE_SCHEMA = z.enum(PROJECT_TYPES).describe("What CommonProject renders: base (default) or obsidian-plugin. Fixed at creation; omit it to keep the recorded type.");
|
|
14430
14540
|
var BOARD_URL_COMPAT_SCHEMA = z.string().optional().describe("Deprecated compatibility input. Ignored; board URLs are derived at runtime and are never persisted.").meta({ deprecated: true });
|
|
14431
14541
|
var RUNTIME_REPO_COMPAT_SCHEMA = z.boolean().optional().describe("Deprecated no-op. Hermes always converges ignored role-local runtime state and never provisions a per-agent GitHub repository.").meta({ deprecated: true });
|
|
14432
14542
|
function safePathSegmentSchema(label) {
|
|
@@ -14748,6 +14858,7 @@ server.registerTool(
|
|
|
14748
14858
|
planeProjectId: z.string().optional(),
|
|
14749
14859
|
projectIdentifier: z.string().optional(),
|
|
14750
14860
|
primaryLanguage: z.string().optional(),
|
|
14861
|
+
projectType: PROJECT_TYPE_SCHEMA.optional(),
|
|
14751
14862
|
skipPlane: z.boolean().optional(),
|
|
14752
14863
|
agentPurpose: z.string().optional(),
|
|
14753
14864
|
local: z.boolean().optional(),
|
|
@@ -14792,7 +14903,8 @@ server.registerTool(
|
|
|
14792
14903
|
targetDir,
|
|
14793
14904
|
projectSlug,
|
|
14794
14905
|
sourceSkill: input.sourceSkill,
|
|
14795
|
-
primaryLanguage: input.primaryLanguage
|
|
14906
|
+
primaryLanguage: input.primaryLanguage,
|
|
14907
|
+
projectType: input.projectType,
|
|
14796
14908
|
apply: !dryRun,
|
|
14797
14909
|
live: input.live ?? false,
|
|
14798
14910
|
provisionTicketBoard: externalEffects.ticketBoard,
|
|
@@ -14857,6 +14969,7 @@ server.registerTool(
|
|
|
14857
14969
|
targetDir: z.string().optional(),
|
|
14858
14970
|
sourceSkill: z.string().optional(),
|
|
14859
14971
|
primaryLanguage: z.string().optional(),
|
|
14972
|
+
projectType: PROJECT_TYPE_SCHEMA.optional(),
|
|
14860
14973
|
apply: z.boolean().optional(),
|
|
14861
14974
|
live: z.boolean().optional(),
|
|
14862
14975
|
provisionTicketBoard: z.boolean().optional().describe("Explicitly opt in to ticket-board provisioning; also requires live=true and skipPlane!=true."),
|
|
@@ -14880,6 +14993,7 @@ server.registerTool(
|
|
|
14880
14993
|
targetDir: input.targetDir,
|
|
14881
14994
|
sourceSkill: input.sourceSkill,
|
|
14882
14995
|
primaryLanguage: input.primaryLanguage,
|
|
14996
|
+
projectType: input.projectType,
|
|
14883
14997
|
apply: input.apply ?? false,
|
|
14884
14998
|
live: input.live ?? false,
|
|
14885
14999
|
provisionTicketBoard: externalEffects.ticketBoard,
|
|
@@ -660,7 +660,7 @@ function validateProjectRecord(project, key) {
|
|
|
660
660
|
function isRecord2(value) {
|
|
661
661
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
662
662
|
}
|
|
663
|
-
var PROJECT_REGISTRY_SCHEMA_VERSION, 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;
|
|
663
|
+
var PROJECT_REGISTRY_SCHEMA_VERSION, PROJECT_TYPE_SENTINELS, 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;
|
|
664
664
|
var init_project = __esm({
|
|
665
665
|
"src/project/index.ts"() {
|
|
666
666
|
"use strict";
|
|
@@ -673,6 +673,9 @@ var init_project = __esm({
|
|
|
673
673
|
init_boardQuery();
|
|
674
674
|
init_registryClient();
|
|
675
675
|
PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
676
|
+
PROJECT_TYPE_SENTINELS = {
|
|
677
|
+
"obsidian-plugin": ["manifest.json", join2("src", "main.ts")]
|
|
678
|
+
};
|
|
676
679
|
PROJECT_IDENTIFIER_SOURCES = ["provider", "proposed"];
|
|
677
680
|
IDENTIFIER_ASSIGNING_PROVIDERS = ["plane", "linear"];
|
|
678
681
|
TICKET_PROVIDER_STATES = ["planned", "linked", "skipped"];
|
|
@@ -234,7 +234,7 @@ function validateManifestFields(manifest, path) {
|
|
|
234
234
|
if (!object(manifest.template.commonproject)) fail("template.commonproject", "an object");
|
|
235
235
|
const template = manifest.template.commonproject;
|
|
236
236
|
if (template.enabled !== void 0 && typeof template.enabled !== "boolean") fail("template.commonproject.enabled", "boolean");
|
|
237
|
-
strings(template, ["primary_language"], "template.commonproject.");
|
|
237
|
+
strings(template, ["primary_language", "project_type"], "template.commonproject.");
|
|
238
238
|
}
|
|
239
239
|
for (const field of ["binding", "policy"]) if (manifest.notebook?.[field] !== void 0 && !object(manifest.notebook[field])) fail(`notebook.${field}`, "an object");
|
|
240
240
|
if (manifest.notebook?.binding) strings(manifest.notebook.binding, ["provider", "state", "notebook_name", "blocked_reason", "notebook_id", "overview_note_id", "created_at", "updated_at"], "notebook.binding.");
|
package/package.json
CHANGED
|
@@ -43,6 +43,30 @@ ticket_provider:
|
|
|
43
43
|
Plane: plane
|
|
44
44
|
Trello: trello
|
|
45
45
|
|
|
46
|
+
# What this project is. `base` is the generic skeleton below and nothing else.
|
|
47
|
+
# `obsidian-plugin` (`pj init --obsidian-plugin`) adds an Obsidian community
|
|
48
|
+
# plugin on top of it, vendored from the canonical starter
|
|
49
|
+
# obsidianmd/obsidian-sample-plugin @ 07ceb81d (2026-08-02): npm + esbuild +
|
|
50
|
+
# TypeScript, manifest.json/versions.json, eslint-plugin-obsidianmd, and its
|
|
51
|
+
# build/release workflows moved to the self-hosted runners.
|
|
52
|
+
#
|
|
53
|
+
# Every type-only path under template/ carries `{% if is_obsidian_plugin %}`
|
|
54
|
+
# in its NAME (directories included), so it does not exist in a base render.
|
|
55
|
+
# Never name one a bare `.gitignore`/`.npmrc`: npm pack drops those from the
|
|
56
|
+
# published pjangler tarball.
|
|
57
|
+
#
|
|
58
|
+
# A real question on purpose, not `when: false`. Copier records only asked
|
|
59
|
+
# questions in .copier-answers.yml; a hidden type would be forgotten, and a
|
|
60
|
+
# re-copy would silently re-render the shared files as `base`. `--defaults`
|
|
61
|
+
# never prompts for it, and pjangler always passes it with --data.
|
|
62
|
+
project_type:
|
|
63
|
+
type: str
|
|
64
|
+
help: What kind of project is this?
|
|
65
|
+
default: base
|
|
66
|
+
choices:
|
|
67
|
+
CommonProject base: base
|
|
68
|
+
Obsidian community plugin: obsidian-plugin
|
|
69
|
+
|
|
46
70
|
# --- Derived (never asked) ---
|
|
47
71
|
|
|
48
72
|
project_slug:
|
|
@@ -50,6 +74,70 @@ project_slug:
|
|
|
50
74
|
default: "{{ project_name | lower | replace(' ', '-') | replace('_', '-') }}"
|
|
51
75
|
when: false
|
|
52
76
|
|
|
77
|
+
is_obsidian_plugin:
|
|
78
|
+
type: bool
|
|
79
|
+
default: "{{ project_type == 'obsidian-plugin' }}"
|
|
80
|
+
when: false
|
|
81
|
+
|
|
82
|
+
# Obsidian manifest identity (obsidian-plugin only). The catalog rejects a
|
|
83
|
+
# plugin id or name containing "obsidian" and frowns on a trailing "plugin",
|
|
84
|
+
# and the id must equal the plugin's folder name in a vault, so both are the
|
|
85
|
+
# project's own identity with those affixes dropped:
|
|
86
|
+
# "obsidian-tag-wrangler" -> id "tag-wrangler".
|
|
87
|
+
plugin_id:
|
|
88
|
+
type: str
|
|
89
|
+
default: >-
|
|
90
|
+
{%- set id = project_slug | lower | regex_replace('[^a-z0-9-]+', '-') | replace('obsidian', '') | regex_replace('-+', '-') -%}
|
|
91
|
+
{%- set id = id.strip('-') -%}
|
|
92
|
+
{%- if id.endswith('-plugin') %}{% set id = id[:-7] %}{% endif -%}
|
|
93
|
+
{{ id or 'plugin' }}
|
|
94
|
+
when: false
|
|
95
|
+
|
|
96
|
+
# A slug-style project name ("obsidian-tag-wrangler") reads as words in the
|
|
97
|
+
# Obsidian UI: separators become spaces and an all-lowercase name is
|
|
98
|
+
# title-cased ("Tag Wrangler"); a name with its own casing keeps it.
|
|
99
|
+
plugin_name:
|
|
100
|
+
type: str
|
|
101
|
+
default: >-
|
|
102
|
+
{%- set name = project_name | regex_replace('(?i)obsidian', ' ') | regex_replace('[-_:.]+', ' ') | regex_replace('\\s+', ' ') | trim -%}
|
|
103
|
+
{%- set name = name | regex_replace('(?i)\\s*\\bplugin$', '') | trim -%}
|
|
104
|
+
{%- if name == name | lower %}{% set name = name | title %}{% endif -%}
|
|
105
|
+
{{ name or project_name }}
|
|
106
|
+
when: false
|
|
107
|
+
|
|
108
|
+
# TypeScript identifier stem for src/: "tag-wrangler" -> TagWranglerPlugin,
|
|
109
|
+
# TagWranglerSettings, TagWranglerSettingTab, TagWranglerModal. The starter's
|
|
110
|
+
# own names (MyPlugin, SampleModal, ...) fail eslint-plugin-obsidianmd's
|
|
111
|
+
# sample-names rule, and a "Plugin" stem would declare PluginSettingTab over
|
|
112
|
+
# Obsidian's own, so those stems get a "Project" suffix.
|
|
113
|
+
plugin_class:
|
|
114
|
+
type: str
|
|
115
|
+
default: >-
|
|
116
|
+
{%- set ns = namespace(name='') -%}
|
|
117
|
+
{%- for word in plugin_id.replace('_', '-').replace('.', '-').split('-') -%}
|
|
118
|
+
{%- set ns.name = ns.name ~ (word | capitalize) -%}
|
|
119
|
+
{%- endfor -%}
|
|
120
|
+
{%- if not ns.name or ns.name[0] in '0123456789' %}{% set ns.name = 'Plugin' ~ ns.name %}{% endif -%}
|
|
121
|
+
{%- if ns.name in ['My', 'Sample', 'Plugin'] %}{% set ns.name = ns.name ~ 'Project' %}{% endif -%}
|
|
122
|
+
{{ ns.name }}
|
|
123
|
+
when: false
|
|
124
|
+
|
|
125
|
+
# The catalog wants a sentence: it must end in punctuation.
|
|
126
|
+
plugin_description:
|
|
127
|
+
type: str
|
|
128
|
+
default: >-
|
|
129
|
+
{%- set text = project_description | trim -%}
|
|
130
|
+
{%- if text and text[-1] not in '.?!)' %}{% set text = text ~ '.' %}{% endif -%}
|
|
131
|
+
{{ text or 'Describe what this plugin does.' }}
|
|
132
|
+
when: false
|
|
133
|
+
|
|
134
|
+
# pjangler passes the operator's git user.name; a bare copier render leaves it
|
|
135
|
+
# for the author to fill in.
|
|
136
|
+
plugin_author:
|
|
137
|
+
type: str
|
|
138
|
+
default: ""
|
|
139
|
+
when: false
|
|
140
|
+
|
|
53
141
|
# Board binding — supplied by init-project.sh's --data-file after the board is
|
|
54
142
|
# created. Defaults keep a bare `copier copy` rendering valid (empty binding).
|
|
55
143
|
plane_workspace:
|
|
@@ -111,6 +199,19 @@ _skip_if_exists:
|
|
|
111
199
|
# Selection is project intent, not generated output. A refresh must preserve
|
|
112
200
|
# inheritance choices, exclusions, and active packs byte-for-byte.
|
|
113
201
|
- ".agents/skills.json"
|
|
202
|
+
# obsidian-plugin seed files: rendered once at creation, owned by the plugin
|
|
203
|
+
# after that. A forced re-render must not reset a bumped manifest version or
|
|
204
|
+
# replace real plugin code with the sample. (Paths a base render never
|
|
205
|
+
# produces, so this is inert for base projects.)
|
|
206
|
+
- "package.json"
|
|
207
|
+
- "package-lock.json"
|
|
208
|
+
- "manifest.json"
|
|
209
|
+
- "versions.json"
|
|
210
|
+
- "styles.css"
|
|
211
|
+
- "tsconfig.json"
|
|
212
|
+
- "esbuild.config.mjs"
|
|
213
|
+
- "eslint.config.mts"
|
|
214
|
+
- "src/**"
|
|
114
215
|
|
|
115
216
|
# --- Post-generation tasks ---
|
|
116
217
|
#
|
|
@@ -124,7 +225,7 @@ _tasks:
|
|
|
124
225
|
# byte and append only the small CommonProject-owned contract when absent.
|
|
125
226
|
# Machine-local backup/editor/CLI exclusions belong in the user's global
|
|
126
227
|
# ignore; `.agents/` is the sole canonical agent-config tree in Git.
|
|
127
|
-
- "touch .gitignore; if [ -s .gitignore ] && [ -n \"$(tail -c 1 .gitignore)\" ]; then printf '\\n' >> .gitignore; fi; grep -Fqx '# CommonProject repository contract' .gitignore 2>/dev/null || printf '%s\\n' '# CommonProject repository contract' >> .gitignore; for pattern in '.env' '.env.*' '!.env.op' '/.agents/local.json' '/.agents/skills'; do grep -Fqx -- \"$pattern\" .gitignore 2>/dev/null || printf '%s\\n' \"$pattern\" >> .gitignore; done"
|
|
228
|
+
- "touch .gitignore; if [ -s .gitignore ] && [ -n \"$(tail -c 1 .gitignore)\" ]; then printf '\\n' >> .gitignore; fi; grep -Fqx '# CommonProject repository contract' .gitignore 2>/dev/null || printf '%s\\n' '# CommonProject repository contract' >> .gitignore; for pattern in '.env' '.env.*' '!.env.op' '/.agents/local.json' '/.agents/skills'{% if is_obsidian_plugin %} 'node_modules/' '/main.js' '*.map' '/data.json'{% endif %}; do grep -Fqx -- \"$pattern\" .gitignore 2>/dev/null || printf '%s\\n' \"$pattern\" >> .gitignore; done"
|
|
128
229
|
# PJAN-82: use the managed script, which refuses to replace a REAL CLAUDE.md.
|
|
129
230
|
# `ln -sf` cannot distinguish a stale link it owns from a hand-written file it
|
|
130
231
|
# must not touch, so a re-render into a live project destroyed the file and
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Build + lint the Obsidian plugin on every push and pull request.
|
|
2
|
+
#
|
|
3
|
+
# Adapted from obsidianmd/obsidian-sample-plugin's lint.yml. The one deliberate
|
|
4
|
+
# change is the runner: self-hosted on big-chungus, never a GitHub-hosted label.
|
|
5
|
+
# Hosted minutes are billed on private repos and the budget is $0. A
|
|
6
|
+
# personal-account repo needs its own repo-level runner (delonet-conventions
|
|
7
|
+
# skill, references/ci-runners.md).
|
|
8
|
+
name: Plugin build
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
push:
|
|
12
|
+
branches: ['**']
|
|
13
|
+
pull_request:
|
|
14
|
+
branches: ['**']
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
build:
|
|
18
|
+
runs-on: [self-hosted, Linux, delonet]
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v6
|
|
22
|
+
- name: Use Node.js 24
|
|
23
|
+
uses: actions/setup-node@v6
|
|
24
|
+
with:
|
|
25
|
+
node-version: 24
|
|
26
|
+
cache: 'npm'
|
|
27
|
+
- run: npm ci
|
|
28
|
+
- run: npm run build --if-present
|
|
29
|
+
- run: npm run lint
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Draft a GitHub release for every pushed tag.
|
|
2
|
+
#
|
|
3
|
+
# Adapted from obsidianmd/obsidian-sample-plugin's release.yml. Changes:
|
|
4
|
+
# - self-hosted runner (see lint.yml for why);
|
|
5
|
+
# - build provenance is attested only on public repos. GitHub refuses
|
|
6
|
+
# artifact attestations on user-owned private repos, and an Obsidian plugin
|
|
7
|
+
# only has to be public once it is submitted to the community catalog.
|
|
8
|
+
#
|
|
9
|
+
# Tag with the bare manifest version (`npm version patch` does this — .npmrc
|
|
10
|
+
# sets tag-version-prefix=""), never a leading `v`: Obsidian matches the release
|
|
11
|
+
# tag against manifest.json's version.
|
|
12
|
+
name: Release Obsidian plugin
|
|
13
|
+
|
|
14
|
+
on:
|
|
15
|
+
push:
|
|
16
|
+
tags:
|
|
17
|
+
- '*'
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
build:
|
|
21
|
+
runs-on: [self-hosted, Linux, delonet]
|
|
22
|
+
permissions:
|
|
23
|
+
contents: write
|
|
24
|
+
id-token: write
|
|
25
|
+
attestations: write
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v6
|
|
28
|
+
|
|
29
|
+
- name: Use Node.js
|
|
30
|
+
uses: actions/setup-node@v6
|
|
31
|
+
with:
|
|
32
|
+
node-version: 24
|
|
33
|
+
cache: 'npm'
|
|
34
|
+
|
|
35
|
+
- name: Build plugin
|
|
36
|
+
run: |
|
|
37
|
+
npm ci
|
|
38
|
+
npm run build
|
|
39
|
+
|
|
40
|
+
- name: Check for optional styles
|
|
41
|
+
id: styles
|
|
42
|
+
run: |
|
|
43
|
+
[ -f styles.css ] && echo "exists=true" >> "$GITHUB_OUTPUT" || echo "exists=false" >> "$GITHUB_OUTPUT"
|
|
44
|
+
|
|
45
|
+
- name: Attest build provenance
|
|
46
|
+
if: ${{ !github.event.repository.private }}
|
|
47
|
+
uses: actions/attest@v4
|
|
48
|
+
with:
|
|
49
|
+
subject-path: |
|
|
50
|
+
main.js
|
|
51
|
+
${{ steps.styles.outputs.exists == 'true' && 'styles.css' || '' }}
|
|
52
|
+
|
|
53
|
+
- name: Create release
|
|
54
|
+
env:
|
|
55
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
56
|
+
run: |
|
|
57
|
+
tag="${GITHUB_REF#refs/tags/}"
|
|
58
|
+
|
|
59
|
+
gh release create "$tag" \
|
|
60
|
+
--title="$tag" \
|
|
61
|
+
--draft \
|
|
62
|
+
main.js manifest.json ${{ steps.styles.outputs.exists == 'true' && 'styles.css' || '' }}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#MISE description="Symlink this plugin into an Obsidian vault: plugin:link [vault] (default: $OBSIDIAN_VAULT)"
|
|
3
|
+
#MISE tools={node="24"}
|
|
4
|
+
# Obsidian loads a plugin from <vault>/.obsidian/plugins/<manifest id>/, reading
|
|
5
|
+
# main.js, manifest.json and styles.css. Linking the repo there means every
|
|
6
|
+
# `npm run dev` rebuild is live after a plugin reload (or instantly with the
|
|
7
|
+
# Hot-Reload community plugin). Replaces only a symlink it can recognise as a
|
|
8
|
+
# link; a real directory at the destination is left alone.
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
root="${MISE_CONFIG_ROOT:-$(pwd)}"
|
|
12
|
+
vault="${1:-${OBSIDIAN_VAULT:-}}"
|
|
13
|
+
[ -n "$vault" ] || { echo "plugin:link: pass the vault path or set OBSIDIAN_VAULT" >&2; exit 2; }
|
|
14
|
+
[ -d "$vault/.obsidian" ] || { echo "plugin:link: $vault is not an Obsidian vault (no .obsidian/)" >&2; exit 2; }
|
|
15
|
+
|
|
16
|
+
id="$(node -p 'JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).id' "$root/manifest.json")"
|
|
17
|
+
[ -n "$id" ] || { echo "plugin:link: manifest.json has no id" >&2; exit 1; }
|
|
18
|
+
|
|
19
|
+
dest="$vault/.obsidian/plugins/$id"
|
|
20
|
+
if [ -e "$dest" ] && [ ! -L "$dest" ]; then
|
|
21
|
+
echo "plugin:link: $dest already exists and is not a symlink; remove it first" >&2
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
mkdir -p "$vault/.obsidian/plugins"
|
|
25
|
+
ln -sfn "$root" "$dest"
|
|
26
|
+
echo "linked $dest -> $root"
|
|
27
|
+
[ -f "$root/main.js" ] || echo "plugin:link: no main.js yet; run npm run dev (or npm run build)" >&2
|
|
@@ -5,3 +5,70 @@
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
7
|
__TODO__
|
|
8
|
+
{%- if is_obsidian_plugin %}
|
|
9
|
+
|
|
10
|
+
## Obsidian community plugin
|
|
11
|
+
|
|
12
|
+
This project is an Obsidian community plugin, scaffolded from the canonical
|
|
13
|
+
[obsidian-sample-plugin](https://github.com/obsidianmd/obsidian-sample-plugin).
|
|
14
|
+
TypeScript in `src/` is bundled by esbuild into `main.js`, which Obsidian loads.
|
|
15
|
+
|
|
16
|
+
- Package manager: **npm** (`package.json` scripts, `package-lock.json`). Never bun or pnpm.
|
|
17
|
+
- Bundler: **esbuild** (`esbuild.config.mjs`). Types come from the `obsidian` package.
|
|
18
|
+
- Release artifacts: `main.js`, `manifest.json`, and `styles.css` (optional).
|
|
19
|
+
|
|
20
|
+
### Commands
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install # dev dependencies, pinned by the committed package-lock.json
|
|
24
|
+
npm run dev # watch build -> main.js (inline sourcemaps)
|
|
25
|
+
npm run build # type-check, then production build
|
|
26
|
+
npm run lint # eslint with eslint-plugin-obsidianmd
|
|
27
|
+
mise run plugin:link # symlink this repo into $OBSIDIAN_VAULT/.obsidian/plugins/<id>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Reload Obsidian (or use the Hot-Reload plugin) and enable the plugin under
|
|
31
|
+
**Settings → Community plugins**.
|
|
32
|
+
|
|
33
|
+
### Layout
|
|
34
|
+
|
|
35
|
+
- `src/main.ts` stays small: plugin lifecycle (`onload`/`onunload`) and registration only.
|
|
36
|
+
- Feature logic goes in focused modules under `src/` (`settings.ts`, `commands/`, `ui/`, `utils/`).
|
|
37
|
+
- Never commit `node_modules/`, `main.js`, `*.map`, or the vault's `data.json`.
|
|
38
|
+
- Bundle every runtime dependency into `main.js`; keep dependencies small and browser-compatible.
|
|
39
|
+
|
|
40
|
+
### Manifest rules (`manifest.json`)
|
|
41
|
+
|
|
42
|
+
- `id` is stable API: never change it after release. It must not contain "obsidian",
|
|
43
|
+
and for local development it must match the plugin folder name in the vault.
|
|
44
|
+
- `version` is SemVer `x.y.z`; keep `minAppVersion` accurate when you use newer APIs.
|
|
45
|
+
- `isDesktopOnly` must be `true` if you use Node or Electron APIs.
|
|
46
|
+
- Canonical checks: https://github.com/obsidianmd/obsidian-releases/blob/master/.github/workflows/validate-plugin-entry.yml
|
|
47
|
+
|
|
48
|
+
### Versioning and releases
|
|
49
|
+
|
|
50
|
+
- `npm version patch|minor|major` is the release step. It bumps `package.json`, and
|
|
51
|
+
`version-bump.mjs` syncs `manifest.json` and `versions.json` (plugin version → minimum
|
|
52
|
+
app version). It commits, then tags the bare version: no leading `v`, because `.npmrc`
|
|
53
|
+
sets `tag-version-prefix=""`.
|
|
54
|
+
- `mise run version:bump` keeps the same three files in step without committing or tagging.
|
|
55
|
+
- Push the tag. `.github/workflows/release.yml` drafts the GitHub release with `main.js`,
|
|
56
|
+
`manifest.json` and `styles.css` attached.
|
|
57
|
+
- The community catalog requires a public repo and a LICENSE; pick one before submitting.
|
|
58
|
+
|
|
59
|
+
### Plugin conventions
|
|
60
|
+
|
|
61
|
+
- Add commands with `this.addCommand(...)` and stable IDs; never rename a released ID.
|
|
62
|
+
- Persist settings with `this.loadData()` / `this.saveData()`, with defaults in `settings.ts`.
|
|
63
|
+
- Register every DOM event, workspace event, and interval through `this.register*`,
|
|
64
|
+
so the plugin unloads without leaking listeners.
|
|
65
|
+
- Keep `onload` light: defer heavy work, debounce vault events, batch disk access.
|
|
66
|
+
- Stay inside the vault, make no network calls without a user-visible reason, and never
|
|
67
|
+
fetch or eval remote code. These are Obsidian's developer policies, and review enforces them.
|
|
68
|
+
- UI copy uses sentence case, short imperative labels, and **Settings → Community plugins**
|
|
69
|
+
style arrow navigation.
|
|
70
|
+
|
|
71
|
+
References: [API docs](https://docs.obsidian.md) ·
|
|
72
|
+
[Plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines) ·
|
|
73
|
+
[Developer policies](https://docs.obsidian.md/Developer+policies)
|
|
74
|
+
{%- endif %}
|