ghcopilot-hub 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +176 -0
  2. package/hub/agents/README.md +243 -0
  3. package/hub/agents/archiver.agent.md +231 -0
  4. package/hub/agents/explore.agent.md +49 -0
  5. package/hub/agents/implementador.agent.md +176 -0
  6. package/hub/agents/librarian.agent.md +34 -0
  7. package/hub/agents/momus.agent.md +130 -0
  8. package/hub/agents/oracle.agent.md +52 -0
  9. package/hub/agents/plan-guardian.agent.md +109 -0
  10. package/hub/agents/planificador.agent.md +295 -0
  11. package/hub/agents/test-sentinel.agent.md +106 -0
  12. package/hub/base/.github/copilot-instructions.md +10 -0
  13. package/hub/base/.github/instructions/ghcopilot-hub.instructions.md +6 -0
  14. package/hub/base/.github/prompts/ghcopilot-hub-maintenance.prompt.md +8 -0
  15. package/hub/base/.vscode/settings.json +1 -0
  16. package/hub/packs/base-web.json +4 -0
  17. package/hub/packs/nextjs-ssr.json +4 -0
  18. package/hub/packs/node-api.json +4 -0
  19. package/hub/packs/spa-tanstack.json +4 -0
  20. package/hub/skills/architecture-testing/SKILL.md +108 -0
  21. package/hub/skills/architecture-testing/references/archunitts.md +46 -0
  22. package/hub/skills/ghcopilot-hub-consumer/SKILL.md +115 -0
  23. package/hub/skills/ghcopilot-hub-consumer/references/workflow.md +39 -0
  24. package/hub/skills/mermaid-expert/SKILL.md +152 -0
  25. package/hub/skills/mermaid-expert/assets/examples/c4_model.md +121 -0
  26. package/hub/skills/mermaid-expert/assets/examples/flowchart.md +123 -0
  27. package/hub/skills/mermaid-expert/assets/examples/img/base_minimal.png +0 -0
  28. package/hub/skills/mermaid-expert/assets/examples/img/corporate.png +0 -0
  29. package/hub/skills/mermaid-expert/assets/examples/img/dark.png +0 -0
  30. package/hub/skills/mermaid-expert/assets/examples/img/dark_neo.png +0 -0
  31. package/hub/skills/mermaid-expert/assets/examples/img/default_neo.png +0 -0
  32. package/hub/skills/mermaid-expert/assets/examples/img/forest_corp.png +0 -0
  33. package/hub/skills/mermaid-expert/assets/examples/img/handdrawn.png +0 -0
  34. package/hub/skills/mermaid-expert/assets/examples/img/neo.png +0 -0
  35. package/hub/skills/mermaid-expert/assets/examples/img/neutral_sketch.png +0 -0
  36. package/hub/skills/mermaid-expert/assets/examples/img/retro.png +0 -0
  37. package/hub/skills/mermaid-expert/assets/examples/sequence.md +116 -0
  38. package/hub/skills/mermaid-expert/assets/examples/styles_and_looks.md +102 -0
  39. package/hub/skills/mermaid-expert/assets/examples/technical.md +130 -0
  40. package/hub/skills/mermaid-expert/assets/examples.md +57 -0
  41. package/hub/skills/mermaid-expert/references/cheatsheet.md +88 -0
  42. package/hub/skills/mermaid-expert/references/validation.md +66 -0
  43. package/hub/skills/react/SKILL.md +235 -0
  44. package/hub/skills/react/references/common-mistakes.md +518 -0
  45. package/hub/skills/react/references/composition-patterns.md +526 -0
  46. package/hub/skills/react/references/effects-patterns.md +396 -0
  47. package/hub/skills/react/references/react-compiler.md +268 -0
  48. package/hub/skills/react-hook-form/SKILL.md +291 -0
  49. package/hub/skills/react-hook-form/references/field-arrays.md +98 -0
  50. package/hub/skills/react-hook-form/references/integration.md +102 -0
  51. package/hub/skills/react-hook-form/references/performance.md +96 -0
  52. package/hub/skills/skill-creator/SKILL.md +152 -0
  53. package/hub/skills/skill-creator/assets/SKILL-TEMPLATE.md +84 -0
  54. package/hub/skills/skill-judge/README.md +261 -0
  55. package/hub/skills/skill-judge/SKILL.md +806 -0
  56. package/hub/skills/tailwind/SKILL.md +200 -0
  57. package/hub/skills/tanstack/SKILL.md +284 -0
  58. package/hub/skills/tanstack/references/loader-adapter-examples.md +79 -0
  59. package/hub/skills/tanstack/references/query-options-examples.md +115 -0
  60. package/hub/skills/tanstack/references/resilience-patterns.md +110 -0
  61. package/hub/skills/tanstack/references/suspense-consumption-examples.md +82 -0
  62. package/hub/skills/tanstack-query/SKILL.md +241 -0
  63. package/hub/skills/tanstack-query/references/advanced-hooks.md +126 -0
  64. package/hub/skills/tanstack-query/references/best-practices.md +241 -0
  65. package/hub/skills/tanstack-query/references/cache-strategies.md +474 -0
  66. package/hub/skills/tanstack-query/references/common-patterns.md +239 -0
  67. package/hub/skills/tanstack-query/references/migration-v5.md +93 -0
  68. package/hub/skills/tanstack-query/references/resilience-and-mutations.md +63 -0
  69. package/hub/skills/tanstack-query/references/testing.md +116 -0
  70. package/hub/skills/tanstack-query/references/top-errors.md +148 -0
  71. package/hub/skills/tanstack-query/references/typescript.md +176 -0
  72. package/hub/skills/tanstack-router/SKILL.md +145 -0
  73. package/hub/skills/tanstack-router/references/code-splitting.md +31 -0
  74. package/hub/skills/tanstack-router/references/errors-and-boundaries.md +44 -0
  75. package/hub/skills/tanstack-router/references/loaders-and-preload.md +51 -0
  76. package/hub/skills/tanstack-router/references/navigation.md +24 -0
  77. package/hub/skills/tanstack-router/references/private-routes.md +169 -0
  78. package/hub/skills/tanstack-router/references/router-context.md +35 -0
  79. package/hub/skills/tanstack-router/references/search-params.md +29 -0
  80. package/hub/skills/tanstack-router/references/typescript.md +24 -0
  81. package/hub/skills/testing/SKILL.md +187 -0
  82. package/hub/skills/testing/references/assertions.md +64 -0
  83. package/hub/skills/testing/references/async-testing.md +66 -0
  84. package/hub/skills/testing/references/e2e-strategy.md +69 -0
  85. package/hub/skills/testing/references/layer-matrix.md +67 -0
  86. package/hub/skills/testing/references/performance.md +49 -0
  87. package/hub/skills/testing/references/tooling-map.md +81 -0
  88. package/hub/skills/testing/references/zustand-mocking.md +84 -0
  89. package/hub/skills/typescript/SKILL.md +232 -0
  90. package/hub/skills/typescript/references/perf-additional-concerns.md +248 -0
  91. package/hub/skills/typescript/references/perf-execution-cache-locality.md +178 -0
  92. package/hub/skills/typescript/references/reduce-branching.md +147 -0
  93. package/hub/skills/typescript/references/reduce-looping.md +203 -0
  94. package/hub/skills/typescript/references/style-and-types.md +171 -0
  95. package/hub/skills/typescript/references/type-vs-interface.md +27 -0
  96. package/hub/skills/zod/SKILL.md +219 -0
  97. package/hub/skills/zustand/SKILL.md +273 -0
  98. package/package.json +59 -0
  99. package/tooling/cli/src/bin.js +11 -0
  100. package/tooling/cli/src/cli.js +409 -0
  101. package/tooling/cli/src/lib/catalog-loader.js +191 -0
  102. package/tooling/cli/src/lib/constants.js +39 -0
  103. package/tooling/cli/src/lib/errors.js +8 -0
  104. package/tooling/cli/src/lib/frontmatter.js +41 -0
  105. package/tooling/cli/src/lib/fs-utils.js +77 -0
  106. package/tooling/cli/src/lib/managed-header.js +74 -0
  107. package/tooling/cli/src/lib/manifest.js +105 -0
  108. package/tooling/cli/src/lib/resolver.js +53 -0
  109. package/tooling/cli/src/lib/sync-engine.js +262 -0
@@ -0,0 +1,77 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export function toPosixPath(value) {
5
+ return value.split(path.sep).join("/");
6
+ }
7
+
8
+ export function fromRoot(rootDir, targetPath) {
9
+ return path.join(rootDir, ...targetPath.split("/"));
10
+ }
11
+
12
+ export function relativeFrom(rootDir, targetPath) {
13
+ return toPosixPath(path.relative(rootDir, targetPath));
14
+ }
15
+
16
+ export async function pathExists(targetPath) {
17
+ try {
18
+ await fs.access(targetPath);
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ export async function ensureDir(targetPath) {
26
+ await fs.mkdir(targetPath, { recursive: true });
27
+ }
28
+
29
+ export async function readTextIfExists(targetPath) {
30
+ if (!(await pathExists(targetPath))) {
31
+ return null;
32
+ }
33
+
34
+ return fs.readFile(targetPath, "utf8");
35
+ }
36
+
37
+ export async function walkFiles(rootDir) {
38
+ if (!(await pathExists(rootDir))) {
39
+ return [];
40
+ }
41
+
42
+ const results = [];
43
+ const entries = await fs.readdir(rootDir, { withFileTypes: true });
44
+
45
+ for (const entry of entries) {
46
+ const absolutePath = path.join(rootDir, entry.name);
47
+
48
+ if (entry.isDirectory()) {
49
+ results.push(...(await walkFiles(absolutePath)));
50
+ continue;
51
+ }
52
+
53
+ if (entry.isFile()) {
54
+ results.push(absolutePath);
55
+ }
56
+ }
57
+
58
+ return results;
59
+ }
60
+
61
+ export async function removeEmptyDirectories(rootDir, stopAtDir) {
62
+ if (!(await pathExists(rootDir))) {
63
+ return;
64
+ }
65
+
66
+ if (path.resolve(rootDir) === path.resolve(stopAtDir)) {
67
+ return;
68
+ }
69
+
70
+ const entries = await fs.readdir(rootDir);
71
+ if (entries.length > 0) {
72
+ return;
73
+ }
74
+
75
+ await fs.rmdir(rootDir);
76
+ await removeEmptyDirectories(path.dirname(rootDir), stopAtDir);
77
+ }
@@ -0,0 +1,74 @@
1
+ import path from "node:path";
2
+ import crypto from "node:crypto";
3
+
4
+ import { MANAGED_BY } from "./constants.js";
5
+
6
+ function getHeaderStyle(targetRelativePath) {
7
+ const extension = path.extname(targetRelativePath).toLowerCase();
8
+
9
+ if (extension === ".json" || extension === ".jsonc" || extension === ".code-workspace") {
10
+ return {
11
+ renderLine: (key, value) => `// ${key}: ${value}`,
12
+ parseLine: (line) => {
13
+ const match = line.match(/^\/\/\s([^:]+):\s(.*)$/);
14
+ return match ? { key: match[1], value: match[2] } : null;
15
+ },
16
+ };
17
+ }
18
+
19
+ return {
20
+ renderLine: (key, value) => `<!-- ${key}: ${value} -->`,
21
+ parseLine: (line) => {
22
+ const match = line.match(/^<!--\s([^:]+):\s(.*)\s-->$/);
23
+ return match ? { key: match[1], value: match[2] } : null;
24
+ },
25
+ };
26
+ }
27
+
28
+ export function hashContent(content) {
29
+ return crypto.createHash("sha256").update(content.replace(/\r\n/g, "\n")).digest("hex");
30
+ }
31
+
32
+ export function renderManagedFile({ targetRelativePath, sourceRelativePath, revision, body }) {
33
+ const style = getHeaderStyle(targetRelativePath);
34
+ const normalizedBody = body.replace(/\r\n/g, "\n");
35
+ const header = [
36
+ style.renderLine("managed-by", MANAGED_BY),
37
+ style.renderLine("source", sourceRelativePath),
38
+ style.renderLine("revision", revision),
39
+ style.renderLine("content-hash", hashContent(normalizedBody)),
40
+ ].join("\n");
41
+
42
+ return `${header}\n\n${normalizedBody}`;
43
+ }
44
+
45
+ export function parseManagedFile({ targetRelativePath, content }) {
46
+ const style = getHeaderStyle(targetRelativePath);
47
+ const normalized = content.replace(/\r\n/g, "\n");
48
+ const lines = normalized.split("\n");
49
+ const header = {};
50
+ let index = 0;
51
+
52
+ while (index < lines.length) {
53
+ const parsedLine = style.parseLine(lines[index]);
54
+ if (!parsedLine) {
55
+ break;
56
+ }
57
+
58
+ header[parsedLine.key] = parsedLine.value;
59
+ index += 1;
60
+ }
61
+
62
+ if (header["managed-by"] !== MANAGED_BY) {
63
+ return null;
64
+ }
65
+
66
+ if (lines[index] === "") {
67
+ index += 1;
68
+ }
69
+
70
+ return {
71
+ header,
72
+ body: lines.slice(index).join("\n"),
73
+ };
74
+ }
@@ -0,0 +1,105 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { CliError } from "./errors.js";
5
+ import { MANIFEST_PATH } from "./constants.js";
6
+ import { ensureDir, fromRoot, pathExists, readTextIfExists } from "./fs-utils.js";
7
+
8
+ export const DEFAULT_MANIFEST = {
9
+ packs: [],
10
+ skills: [],
11
+ excludeSkills: [],
12
+ settings: {
13
+ onConflict: "fail",
14
+ preserveLocalOverrides: true,
15
+ },
16
+ };
17
+
18
+ export function normalizeManifest(rawManifest) {
19
+ const manifest = structuredClone(DEFAULT_MANIFEST);
20
+ const raw = rawManifest ?? {};
21
+
22
+ manifest.packs = normalizeStringArray(raw.packs, "packs");
23
+ manifest.skills = normalizeStringArray(raw.skills, "skills");
24
+ manifest.excludeSkills = normalizeStringArray(raw.excludeSkills, "excludeSkills");
25
+
26
+ const rawSettings = raw.settings ?? {};
27
+ const onConflict = rawSettings.onConflict ?? DEFAULT_MANIFEST.settings.onConflict;
28
+
29
+ if (!["fail", "overwrite"].includes(onConflict)) {
30
+ throw new CliError('Manifest setting "settings.onConflict" must be "fail" or "overwrite".');
31
+ }
32
+
33
+ manifest.settings = {
34
+ onConflict,
35
+ preserveLocalOverrides:
36
+ typeof rawSettings.preserveLocalOverrides === "boolean"
37
+ ? rawSettings.preserveLocalOverrides
38
+ : DEFAULT_MANIFEST.settings.preserveLocalOverrides,
39
+ };
40
+
41
+ return manifest;
42
+ }
43
+
44
+ export async function readManifest(projectDir, options = {}) {
45
+ const manifestPath = fromRoot(projectDir, MANIFEST_PATH);
46
+ const manifestText = await readTextIfExists(manifestPath);
47
+
48
+ if (manifestText === null) {
49
+ if (options.allowMissing) {
50
+ return null;
51
+ }
52
+
53
+ throw new CliError(`Manifest not found at ${MANIFEST_PATH}. Run "ghcopilot-hub init" first.`);
54
+ }
55
+
56
+ let parsed;
57
+ try {
58
+ parsed = JSON.parse(manifestText);
59
+ } catch (error) {
60
+ throw new CliError(`Manifest at ${MANIFEST_PATH} is not valid JSON: ${error.message}`);
61
+ }
62
+
63
+ return normalizeManifest(parsed);
64
+ }
65
+
66
+ export async function writeManifest(projectDir, manifest) {
67
+ const manifestPath = fromRoot(projectDir, MANIFEST_PATH);
68
+ await ensureDir(path.dirname(manifestPath));
69
+ await fs.writeFile(
70
+ manifestPath,
71
+ `${JSON.stringify(normalizeManifest(manifest), null, 2)}\n`,
72
+ "utf8"
73
+ );
74
+ }
75
+
76
+ export async function ensureManifest(projectDir) {
77
+ const manifestPath = fromRoot(projectDir, MANIFEST_PATH);
78
+ if (!(await pathExists(manifestPath))) {
79
+ await writeManifest(projectDir, DEFAULT_MANIFEST);
80
+ }
81
+
82
+ return readManifest(projectDir);
83
+ }
84
+
85
+ function normalizeStringArray(value, fieldName) {
86
+ if (value === undefined) {
87
+ return [];
88
+ }
89
+
90
+ if (!Array.isArray(value)) {
91
+ throw new CliError(`Manifest field "${fieldName}" must be an array of strings.`);
92
+ }
93
+
94
+ return [
95
+ ...new Set(
96
+ value.map((item) => {
97
+ if (typeof item !== "string" || item.trim() === "") {
98
+ throw new CliError(`Manifest field "${fieldName}" must contain only non-empty strings.`);
99
+ }
100
+
101
+ return item.trim();
102
+ })
103
+ ),
104
+ ].sort();
105
+ }
@@ -0,0 +1,53 @@
1
+ import { CliError } from "./errors.js";
2
+
3
+ const DEFAULT_SKILL_IDS = ["ghcopilot-hub-consumer"];
4
+
5
+ export function resolveProjectState({ catalog, manifest }) {
6
+ const packMap = new Map(catalog.packs.map((pack) => [pack.name, pack]));
7
+ const skillMap = new Map(catalog.skills.map((skill) => [skill.id, skill]));
8
+
9
+ const resolvedSkillIds = new Set();
10
+
11
+ for (const skillId of DEFAULT_SKILL_IDS) {
12
+ if (!skillMap.has(skillId)) {
13
+ throw new CliError(`Hub is missing required default skill "${skillId}".`);
14
+ }
15
+
16
+ resolvedSkillIds.add(skillId);
17
+ }
18
+
19
+ for (const packName of manifest.packs) {
20
+ const pack = packMap.get(packName);
21
+ if (!pack) {
22
+ throw new CliError(`Manifest references unknown pack "${packName}".`);
23
+ }
24
+
25
+ for (const skillId of pack.skills) {
26
+ resolvedSkillIds.add(skillId);
27
+ }
28
+ }
29
+
30
+ for (const skillId of manifest.skills) {
31
+ if (!skillMap.has(skillId)) {
32
+ throw new CliError(`Manifest references unknown skill "${skillId}".`);
33
+ }
34
+
35
+ resolvedSkillIds.add(skillId);
36
+ }
37
+
38
+ for (const skillId of manifest.excludeSkills) {
39
+ if (!skillMap.has(skillId)) {
40
+ throw new CliError(`Manifest excludes unknown skill "${skillId}".`);
41
+ }
42
+
43
+ resolvedSkillIds.delete(skillId);
44
+ }
45
+
46
+ const skills = [...resolvedSkillIds].sort().map((skillId) => skillMap.get(skillId));
47
+
48
+ return {
49
+ agents: catalog.agents,
50
+ skills,
51
+ baseFiles: catalog.baseFiles,
52
+ };
53
+ }
@@ -0,0 +1,262 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { CliError } from "./errors.js";
5
+ import { MANAGED_ROOTS, getRequiredDirs } from "./constants.js";
6
+ import {
7
+ ensureDir,
8
+ fromRoot,
9
+ pathExists,
10
+ readTextIfExists,
11
+ removeEmptyDirectories,
12
+ walkFiles,
13
+ } from "./fs-utils.js";
14
+ import { hashContent, parseManagedFile, renderManagedFile } from "./managed-header.js";
15
+
16
+ export async function createDesiredFiles({ resolvedState, revision }) {
17
+ const desiredFiles = [];
18
+
19
+ for (const baseFile of resolvedState.baseFiles) {
20
+ desiredFiles.push(await materializeDesiredFile(baseFile, revision));
21
+ }
22
+
23
+ for (const agent of resolvedState.agents) {
24
+ desiredFiles.push(await materializeDesiredFile(agent, revision));
25
+ }
26
+
27
+ for (const skill of resolvedState.skills) {
28
+ for (const file of skill.files) {
29
+ desiredFiles.push(await materializeDesiredFile(file, revision));
30
+ }
31
+ }
32
+
33
+ return desiredFiles.sort((left, right) =>
34
+ left.targetRelativePath.localeCompare(right.targetRelativePath)
35
+ );
36
+ }
37
+
38
+ export async function planProjectSync({ projectDir, desiredFiles, onConflict }) {
39
+ const desiredMap = new Map(desiredFiles.map((file) => [file.targetRelativePath, file]));
40
+ const existingManagedEntries = await scanManagedEntries(projectDir);
41
+
42
+ const create = [];
43
+ const update = [];
44
+ const remove = [];
45
+ const conflicts = [];
46
+ const unchanged = [];
47
+ const missingManaged = [];
48
+ const drifted = [];
49
+ const unexpectedManaged = [];
50
+ const unexpectedUnmanaged = [];
51
+
52
+ for (const desiredFile of desiredFiles) {
53
+ const absolutePath = fromRoot(projectDir, desiredFile.targetRelativePath);
54
+ const currentContent = await readTextIfExists(absolutePath);
55
+
56
+ if (currentContent === null) {
57
+ create.push({ targetRelativePath: desiredFile.targetRelativePath, reason: "missing" });
58
+ missingManaged.push(desiredFile.targetRelativePath);
59
+ continue;
60
+ }
61
+
62
+ if (currentContent === desiredFile.renderedContent) {
63
+ unchanged.push({ targetRelativePath: desiredFile.targetRelativePath });
64
+ continue;
65
+ }
66
+
67
+ const parsedManaged = parseManagedFile({
68
+ targetRelativePath: desiredFile.targetRelativePath,
69
+ content: currentContent,
70
+ });
71
+
72
+ if (!parsedManaged) {
73
+ conflicts.push({
74
+ targetRelativePath: desiredFile.targetRelativePath,
75
+ reason: "expected managed file but found unmanaged content",
76
+ });
77
+ unexpectedUnmanaged.push(desiredFile.targetRelativePath);
78
+ continue;
79
+ }
80
+
81
+ if (parsedManaged.body === desiredFile.body) {
82
+ update.push({
83
+ targetRelativePath: desiredFile.targetRelativePath,
84
+ reason: "revision metadata changed",
85
+ });
86
+ continue;
87
+ }
88
+
89
+ const currentHash = hashContent(parsedManaged.body);
90
+ const isLocallyDrifted = parsedManaged.header["content-hash"] !== currentHash;
91
+
92
+ if (isLocallyDrifted && onConflict !== "overwrite") {
93
+ conflicts.push({
94
+ targetRelativePath: desiredFile.targetRelativePath,
95
+ reason: "managed file drifted locally",
96
+ });
97
+ drifted.push(desiredFile.targetRelativePath);
98
+ continue;
99
+ }
100
+
101
+ update.push({
102
+ targetRelativePath: desiredFile.targetRelativePath,
103
+ reason: isLocallyDrifted ? "overwrite drifted managed file" : "hub content changed",
104
+ });
105
+
106
+ if (isLocallyDrifted) {
107
+ drifted.push(desiredFile.targetRelativePath);
108
+ }
109
+ }
110
+
111
+ for (const entry of existingManagedEntries) {
112
+ if (desiredMap.has(entry.targetRelativePath)) {
113
+ continue;
114
+ }
115
+
116
+ if (entry.kind === "managed") {
117
+ const isLocallyDrifted = entry.header["content-hash"] !== hashContent(entry.body);
118
+ if (isLocallyDrifted && onConflict !== "overwrite") {
119
+ conflicts.push({
120
+ targetRelativePath: entry.targetRelativePath,
121
+ reason: "managed file scheduled for deletion but drifted locally",
122
+ });
123
+ drifted.push(entry.targetRelativePath);
124
+ continue;
125
+ }
126
+
127
+ remove.push({
128
+ targetRelativePath: entry.targetRelativePath,
129
+ reason: "orphaned managed file",
130
+ });
131
+ continue;
132
+ }
133
+
134
+ unexpectedUnmanaged.push(entry.targetRelativePath);
135
+ }
136
+
137
+ for (const entry of existingManagedEntries) {
138
+ if (entry.kind === "managed" && !desiredMap.has(entry.targetRelativePath)) {
139
+ unexpectedManaged.push(entry.targetRelativePath);
140
+ }
141
+ }
142
+
143
+ return {
144
+ create,
145
+ update,
146
+ remove,
147
+ unchanged,
148
+ conflicts,
149
+ diagnostics: {
150
+ missingManaged,
151
+ drifted: [...new Set(drifted)].sort(),
152
+ unexpectedManaged: [...new Set(unexpectedManaged)].sort(),
153
+ unexpectedUnmanaged: [...new Set(unexpectedUnmanaged)].sort(),
154
+ },
155
+ };
156
+ }
157
+
158
+ export async function applyProjectSync({ projectDir, desiredFiles, plan, preserveLocalOverrides }) {
159
+ if (plan.conflicts.length > 0) {
160
+ const summary = plan.conflicts
161
+ .map((conflict) => `${conflict.targetRelativePath}: ${conflict.reason}`)
162
+ .join("\n");
163
+ throw new CliError(`Cannot apply sync because conflicts were detected:\n${summary}`);
164
+ }
165
+
166
+ const requiredDirs = getRequiredDirs(preserveLocalOverrides);
167
+
168
+ for (const requiredDir of requiredDirs) {
169
+ await ensureDir(fromRoot(projectDir, requiredDir));
170
+ }
171
+
172
+ const desiredMap = new Map(desiredFiles.map((file) => [file.targetRelativePath, file]));
173
+
174
+ for (const operation of [...plan.create, ...plan.update]) {
175
+ const file = desiredMap.get(operation.targetRelativePath);
176
+ const targetPath = fromRoot(projectDir, operation.targetRelativePath);
177
+ await ensureDir(path.dirname(targetPath));
178
+ await fs.writeFile(targetPath, file.renderedContent, "utf8");
179
+ }
180
+
181
+ for (const operation of plan.remove) {
182
+ const targetPath = fromRoot(projectDir, operation.targetRelativePath);
183
+ if (await pathExists(targetPath)) {
184
+ await fs.rm(targetPath, { force: true });
185
+ await removeEmptyDirectories(path.dirname(targetPath), projectDir);
186
+ }
187
+ }
188
+
189
+ for (const requiredDir of requiredDirs) {
190
+ await ensureDir(fromRoot(projectDir, requiredDir));
191
+ }
192
+ }
193
+
194
+ export async function validateManagedProject({ projectDir, desiredFiles, onConflict }) {
195
+ return planProjectSync({ projectDir, desiredFiles, onConflict });
196
+ }
197
+
198
+ async function materializeDesiredFile(file, revision) {
199
+ const body = await fs.readFile(file.sourcePath, "utf8");
200
+
201
+ return {
202
+ sourcePath: file.sourcePath,
203
+ sourceRelativePath: file.sourceRelativePath,
204
+ targetRelativePath: file.targetRelativePath,
205
+ body,
206
+ renderedContent: renderManagedFile({
207
+ targetRelativePath: file.targetRelativePath,
208
+ sourceRelativePath: file.sourceRelativePath,
209
+ revision,
210
+ body,
211
+ }),
212
+ };
213
+ }
214
+
215
+ async function scanManagedEntries(projectDir) {
216
+ const entries = [];
217
+
218
+ for (const managedRoot of MANAGED_ROOTS) {
219
+ const absoluteRoot = fromRoot(projectDir, managedRoot);
220
+ if (!(await pathExists(absoluteRoot))) {
221
+ continue;
222
+ }
223
+
224
+ const stat = await fs.stat(absoluteRoot);
225
+ const files = stat.isDirectory() ? await walkFiles(absoluteRoot) : [absoluteRoot];
226
+
227
+ for (const absolutePath of files) {
228
+ const targetRelativePath = path.relative(projectDir, absolutePath).split(path.sep).join("/");
229
+ const content = await fs.readFile(absolutePath, "utf8");
230
+ const parsedManaged = parseManagedFile({ targetRelativePath, content });
231
+
232
+ if (parsedManaged) {
233
+ entries.push({
234
+ kind: "managed",
235
+ targetRelativePath,
236
+ header: parsedManaged.header,
237
+ body: parsedManaged.body,
238
+ });
239
+ continue;
240
+ }
241
+
242
+ entries.push({
243
+ kind: "unmanaged",
244
+ targetRelativePath,
245
+ content,
246
+ });
247
+ }
248
+ }
249
+
250
+ return entries.filter((entry) => entry.targetRelativePath !== ".github/ghcopilot-hub.json");
251
+ }
252
+
253
+ export function summarizePlan(plan) {
254
+ return {
255
+ create: plan.create.length,
256
+ update: plan.update.length,
257
+ remove: plan.remove.length,
258
+ unchanged: plan.unchanged.length,
259
+ conflicts: plan.conflicts.length,
260
+ drifted: plan.diagnostics.drifted.length,
261
+ };
262
+ }