fraim 2.0.248 → 2.0.249

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.
@@ -6,32 +6,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.managerPackRelativePathFor = managerPackRelativePathFor;
7
7
  exports.publishManagerArtifacts = publishManagerArtifacts;
8
8
  /**
9
- * Publish manager context/rules/learnings to the configured manager backend
10
- * (issue #580).
9
+ * Publish manager context, rules, learnings, capabilities, and employees to the
10
+ * configured manager backend (issue #580; capability and employee support and
11
+ * pull-request creation added in issue #1043).
11
12
  */
12
13
  const axios_1 = __importDefault(require("axios"));
13
- const child_process_1 = require("child_process");
14
- const fs_1 = __importDefault(require("fs"));
15
- const os_1 = __importDefault(require("os"));
16
14
  const path_1 = __importDefault(require("path"));
17
15
  const manager_pack_1 = require("../../core/manager-pack");
18
16
  const user_config_1 = require("./user-config");
19
17
  const local_folder_sync_1 = require("./local-folder-sync");
20
- const ALLOWED_GIT_URL = /^(https?:\/\/|ssh:\/\/|git:\/\/|file:\/\/|[\w.-]+@[\w.-]+:)/;
18
+ const pack_git_publish_1 = require("./pack-git-publish");
19
+ const pack_home_1 = require("./pack-home");
21
20
  function managerPackRelativePathFor(filePath) {
22
21
  const relativePath = (0, manager_pack_1.managerPackRelativePathForFileName)(filePath);
23
22
  if (relativePath)
24
23
  return relativePath;
25
- // Capability files (jobs/skills/rules/templates/scripts) carry their pack-relative
26
- // path verbatim — the caller supplies the full relative path, not just a basename.
27
24
  const base = path_1.default.basename(filePath);
28
- throw new Error(`Cannot infer manager-pack location for '${base}' (expected manager_context.md, manager_rules.md, a personal learning file, or a full capability path like jobs/<cat>/<name>.md)`);
29
- }
30
- function gitCompareUrl(gitUrl, branch) {
31
- const httpUrl = gitUrl.replace(/\.git$/, '').replace(/^git@([^:]+):/, 'https://$1/');
32
- if (/^https?:\/\//.test(httpUrl))
33
- return `${httpUrl}/compare/${branch}?expand=1`;
34
- return undefined;
25
+ throw new Error(`Cannot infer manager-pack location for '${base}' (expected manager_context.md, manager_rules.md, a personal learning file, or a full capability path like jobs/<cat>/<name>.md or employees/<slug>.json)`);
35
26
  }
36
27
  async function publishManagerArtifacts(artifacts, opts) {
37
28
  const managerStorage = (0, user_config_1.getManagerStorageConfig)();
@@ -42,7 +33,13 @@ async function publishManagerArtifacts(artifacts, opts) {
42
33
  throw new Error('No artifacts to publish.');
43
34
  for (const artifact of artifacts) {
44
35
  if (!(0, manager_pack_1.isManagerPackRelativePath)(artifact.relativePath)) {
45
- throw new Error(`Invalid manager-pack path '${artifact.relativePath}' (must be context/manager_context.md, rules/manager_rules.md, or learnings/<user>-*.md).`);
36
+ throw new Error(`Invalid manager-pack path '${artifact.relativePath}' (must be context/manager_context.md, rules/manager_rules.md, learnings/<user>-*.md, or a capability path like jobs/<cat>/<name>.md or employees/<slug>.json).`);
37
+ }
38
+ }
39
+ const deletions = opts?.deletions ?? [];
40
+ for (const relativePath of deletions) {
41
+ if (!(0, manager_pack_1.isManagerPackRelativePath)(relativePath)) {
42
+ throw new Error(`Invalid manager-pack deletion path '${relativePath}'.`);
46
43
  }
47
44
  }
48
45
  if (managerStorage.backend === 'local-folder') {
@@ -55,7 +52,8 @@ async function publishManagerArtifacts(artifacts, opts) {
55
52
  }
56
53
  writtenPaths.push(path_1.default.join(localPath, artifact.relativePath));
57
54
  }
58
- return { backend: 'local-folder', writtenPaths };
55
+ const deletedPaths = (0, local_folder_sync_1.removeLocalFolderFiles)(localPath, deletions, artifacts.map((a) => a.relativePath));
56
+ return { backend: 'local-folder', writtenPaths, deletedPaths };
59
57
  }
60
58
  if (managerStorage.backend === 'fraim-cloud') {
61
59
  const remoteUrl = opts?.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
@@ -65,27 +63,27 @@ async function publishManagerArtifacts(artifacts, opts) {
65
63
  const res = await axios_1.default.post(`${remoteUrl.replace(/\/$/, '')}/api/manager/publish`, { artifacts }, { headers: { 'x-api-key': apiKey }, timeout: 30_000 });
66
64
  return { backend: 'fraim-cloud', version: String(res.data?.version ?? '') };
67
65
  }
68
- const gitUrl = managerStorage.gitUrl;
69
- if (!ALLOWED_GIT_URL.test(gitUrl))
70
- throw new Error(`Manager repo URL has an unsupported scheme: ${gitUrl}`);
71
- const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'fraim-manager-pub-'));
72
- try {
73
- (0, child_process_1.execFileSync)('git', ['clone', '--depth=1', '--quiet', '--', gitUrl, '.'], {
74
- cwd: dir, stdio: ['ignore', 'pipe', 'pipe'], timeout: 60_000
75
- });
76
- const branch = `fraim-manager-update-${opts?.branchSuffix || `${process.pid}-${Date.now()}`}`;
77
- (0, child_process_1.execFileSync)('git', ['checkout', '-b', branch], { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'] });
78
- for (const artifact of artifacts) {
79
- const dest = path_1.default.join(dir, artifact.relativePath);
80
- fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
81
- fs_1.default.writeFileSync(dest, artifact.content);
82
- }
83
- (0, child_process_1.execFileSync)('git', ['add', '-A'], { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'] });
84
- (0, child_process_1.execFileSync)('git', ['commit', '-m', 'Update manager context'], { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'] });
85
- (0, child_process_1.execFileSync)('git', ['push', '--quiet', 'origin', branch], { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'], timeout: 60_000 });
86
- return { backend: 'git', branch, prUrl: gitCompareUrl(gitUrl, branch) };
87
- }
88
- finally {
89
- fs_1.default.rmSync(dir, { recursive: true, force: true });
90
- }
66
+ const change = (0, pack_git_publish_1.describePackChange)('manager', artifacts, deletions);
67
+ const published = await (0, pack_git_publish_1.publishPackToGitRepo)({
68
+ gitUrl: managerStorage.gitUrl,
69
+ artifacts,
70
+ deletions,
71
+ branchSuffix: opts?.branchSuffix,
72
+ branchPrefix: 'fraim-manager-update-',
73
+ commitMessage: change.title,
74
+ prTitle: change.title,
75
+ prBody: change.body,
76
+ openPullRequest: opts?.openPullRequest,
77
+ // Publish from the durable pack home, not a throwaway clone.
78
+ cloneDir: (0, pack_home_1.resolvePackHome)('manager').root,
79
+ });
80
+ return {
81
+ backend: 'git',
82
+ branch: published.branch,
83
+ prUrl: published.prUrl,
84
+ prCreated: published.prCreated,
85
+ prBlockedReason: published.prBlockedReason,
86
+ credentialWarning: published.credentialWarning,
87
+ deletedPaths: deletions,
88
+ };
91
89
  }
@@ -3,12 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.ORG_CACHE_MANAGED_HEADER = exports.ORG_SYNC_METADATA_FILE = exports.ORG_CACHE_DIRNAME = exports.shouldDecorateAsMarkdown = void 0;
6
+ exports.ORG_MANAGED_HEADER = exports.ORG_SYNC_METADATA_FILE = exports.ORG_HOME_DIRNAME = exports.shouldDecorateAsMarkdown = void 0;
7
7
  exports.isSafePackPath = isSafePackPath;
8
8
  exports.isSafeOrgCapabilityPackPath = isSafeOrgCapabilityPackPath;
9
- exports.getOrgCacheDir = getOrgCacheDir;
10
- exports.readOrgCacheMetadata = readOrgCacheMetadata;
11
- exports.getOrgCacheAgeHours = getOrgCacheAgeHours;
9
+ exports.getOrgHomeDir = getOrgHomeDir;
10
+ exports.readOrgSyncMetadata = readOrgSyncMetadata;
11
+ exports.getOrgSyncAgeHours = getOrgSyncAgeHours;
12
12
  exports.syncOrgCache = syncOrgCache;
13
13
  /**
14
14
  * Org cache sync (issue #563).
@@ -27,16 +27,20 @@ const fs_1 = __importDefault(require("fs"));
27
27
  const path_1 = __importDefault(require("path"));
28
28
  const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
29
29
  const user_config_1 = require("./user-config");
30
- const git_org_sync_1 = require("./git-org-sync");
30
+ const pack_home_1 = require("./pack-home");
31
31
  const local_folder_sync_1 = require("./local-folder-sync");
32
32
  const capability_pack_1 = require("../../core/capability-pack");
33
33
  var capability_pack_2 = require("../../core/capability-pack");
34
34
  Object.defineProperty(exports, "shouldDecorateAsMarkdown", { enumerable: true, get: function () { return capability_pack_2.shouldDecorateAsMarkdown; } });
35
- exports.ORG_CACHE_DIRNAME = 'org';
35
+ exports.ORG_HOME_DIRNAME = 'org';
36
36
  exports.ORG_SYNC_METADATA_FILE = '.org-sync-metadata.json';
37
- exports.ORG_CACHE_MANAGED_HEADER = '<!-- FRAIM_ORG_SYNC_MANAGED_CONTENT -->';
38
- /** Subdirectories of the org pack that sync into the cache (spec R7.1). */
39
- const ORG_PACK_DIRS = ['context', 'rules', 'learnings', 'jobs', 'skills', 'templates'];
37
+ exports.ORG_MANAGED_HEADER = '<!-- FRAIM_ORG_SYNC_MANAGED_CONTENT -->';
38
+ /**
39
+ * Subdirectories of the org pack that sync into the cache (spec R7.1).
40
+ * Issue #1043 adds 'employees' so an org-shared employee record reaches the
41
+ * cache that custom-employees.ts reads.
42
+ */
43
+ const ORG_PACK_DIRS = ['context', 'rules', 'learnings', 'jobs', 'skills', 'templates', 'employees'];
40
44
  /**
41
45
  * Pack files must stay inside the three org pack directories. Applied to
42
46
  * every relativePath before it touches the filesystem, so a compromised
@@ -70,22 +74,51 @@ function isSafeOrgCapabilityPackPath(relativePath) {
70
74
  return false;
71
75
  return true;
72
76
  }
73
- function getOrgCacheDir() {
74
- return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), exports.ORG_CACHE_DIRNAME);
77
+ /**
78
+ * The layer's single home. Named `getOrgHomeDir` for backward compatibility with
79
+ * existing callers; issue #1043 round 2 removed the separate cache it used to
80
+ * point at, so this now resolves the one place the layer lives.
81
+ */
82
+ function getOrgHomeDir() {
83
+ return (0, pack_home_1.resolvePackHome)('org').root;
84
+ }
85
+ /**
86
+ * Where sync provenance is recorded.
87
+ *
88
+ * Deliberately NOT inside the home. The home is the user's own content: for a
89
+ * git backend it is a real working tree, so a metadata file there shows up as
90
+ * an untracked change in their pack repo, and for a synced folder it would
91
+ * replicate machine-local bookkeeping to every other machine. Provenance is
92
+ * local state, so it belongs in ~/.fraim beside the config.
93
+ */
94
+ function syncMetadataPath() {
95
+ return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), exports.ORG_SYNC_METADATA_FILE);
96
+ }
97
+ /** Record when and from where this machine last synced. Never touches the home. */
98
+ function writeHomeMetadata(metadata) {
99
+ try {
100
+ fs_1.default.mkdirSync((0, project_fraim_paths_1.getUserFraimDirPath)(), { recursive: true });
101
+ fs_1.default.writeFileSync(syncMetadataPath(), JSON.stringify(metadata, null, 2));
102
+ }
103
+ catch { /* provenance is best-effort; never fail a sync over it */ }
75
104
  }
76
- function readOrgCacheMetadata() {
105
+ function readOrgSyncMetadata() {
77
106
  try {
78
- const metadataPath = path_1.default.join(getOrgCacheDir(), exports.ORG_SYNC_METADATA_FILE);
79
- if (!fs_1.default.existsSync(metadataPath))
107
+ const metadataPath = syncMetadataPath();
108
+ if (fs_1.default.existsSync(metadataPath))
109
+ return JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf8'));
110
+ // A machine that last synced before provenance moved out of the home.
111
+ const legacy = path_1.default.join(getOrgHomeDir(), exports.ORG_SYNC_METADATA_FILE);
112
+ if (!fs_1.default.existsSync(legacy))
80
113
  return null;
81
- return JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf8'));
114
+ return JSON.parse(fs_1.default.readFileSync(legacy, 'utf8'));
82
115
  }
83
116
  catch {
84
117
  return null;
85
118
  }
86
119
  }
87
- function getOrgCacheAgeHours() {
88
- const metadata = readOrgCacheMetadata();
120
+ function getOrgSyncAgeHours() {
121
+ const metadata = readOrgSyncMetadata();
89
122
  if (!metadata?.syncedAt)
90
123
  return null;
91
124
  const syncedAt = Date.parse(metadata.syncedAt);
@@ -95,7 +128,7 @@ function getOrgCacheAgeHours() {
95
128
  }
96
129
  function decorateManagedOrgFile(content, backend) {
97
130
  const normalized = content.replace(/^/, '');
98
- if (normalized.startsWith(exports.ORG_CACHE_MANAGED_HEADER))
131
+ if (normalized.startsWith(exports.ORG_MANAGED_HEADER))
99
132
  return normalized;
100
133
  const writePath = backend === 'git'
101
134
  ? 'open a pull request against your organization repo'
@@ -103,7 +136,7 @@ function decorateManagedOrgFile(content, backend) {
103
136
  ? 'edit the file in your synced folder and let the sync client upload it'
104
137
  : 'update it through the organization-onboarding flow';
105
138
  const marker = [
106
- exports.ORG_CACHE_MANAGED_HEADER,
139
+ exports.ORG_MANAGED_HEADER,
107
140
  '> [!IMPORTANT]',
108
141
  '> Synced from your organization\'s shared context. Local edits are overwritten on the next sync.',
109
142
  `> To change this content, ${writePath}.`,
@@ -145,8 +178,8 @@ async function fetchCloudPack(remoteUrl, apiKey) {
145
178
  };
146
179
  }
147
180
  /** Replace the cache contents atomically: stage fully, then swap. */
148
- function materializeCache(files, metadata) {
149
- const cacheDir = getOrgCacheDir();
181
+ function materializeCloudHome(files, metadata) {
182
+ const cacheDir = getOrgHomeDir();
150
183
  const stagingDir = `${cacheDir}.staging-${process.pid}`;
151
184
  fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
152
185
  fs_1.default.mkdirSync(stagingDir, { recursive: true });
@@ -162,7 +195,7 @@ function materializeCache(files, metadata) {
162
195
  : file.content;
163
196
  fs_1.default.writeFileSync(destination, content);
164
197
  }
165
- fs_1.default.writeFileSync(path_1.default.join(stagingDir, exports.ORG_SYNC_METADATA_FILE), JSON.stringify(metadata, null, 2));
198
+ // Provenance is written outside the home by writeHomeMetadata.
166
199
  fs_1.default.rmSync(cacheDir, { recursive: true, force: true });
167
200
  fs_1.default.renameSync(stagingDir, cacheDir);
168
201
  }
@@ -191,12 +224,12 @@ async function discoverCloudOrganization(options) {
191
224
  }
192
225
  function failureOutcome(error) {
193
226
  const message = error instanceof Error ? error.message : String(error);
194
- const existing = readOrgCacheMetadata();
227
+ const existing = readOrgSyncMetadata();
195
228
  if (existing) {
196
229
  return {
197
230
  status: 'stale',
198
231
  metadata: existing,
199
- ageHours: getOrgCacheAgeHours() ?? 0,
232
+ ageHours: getOrgSyncAgeHours() ?? 0,
200
233
  error: message
201
234
  };
202
235
  }
@@ -221,32 +254,35 @@ async function syncOrgCache(options) {
221
254
  }
222
255
  try {
223
256
  if (organization.backend === 'local-folder') {
257
+ // Issue #1043 round 2: the configured folder IS the home. There is
258
+ // no cache to materialize into; the OS sync client is the transport
259
+ // and the files are already the single place FRAIM reads.
224
260
  const localPath = organization.localPath;
225
- const files = (0, local_folder_sync_1.collectLocalFolderFiles)(localPath, (rel) => isSafePackPath(rel) || isSafeOrgCapabilityPackPath(rel));
226
261
  const metadata = {
227
262
  version: (0, local_folder_sync_1.localFolderVersion)(localPath),
228
263
  backend: 'local-folder',
229
264
  source: localPath,
230
265
  syncedAt: new Date().toISOString(),
231
266
  };
232
- materializeCache(files, metadata);
267
+ writeHomeMetadata(metadata);
233
268
  return { status: 'synced', metadata };
234
269
  }
235
270
  if (organization.backend === 'git') {
236
- const snapshot = (0, git_org_sync_1.fetchOrgRepoSnapshot)(organization.gitUrl);
237
- try {
238
- const metadata = {
239
- version: snapshot.sha,
240
- backend: 'git',
241
- source: organization.gitUrl,
242
- syncedAt: new Date().toISOString()
243
- };
244
- materializeCache(collectGitPackFiles(snapshot.dir), metadata);
245
- return { status: 'synced', metadata };
246
- }
247
- finally {
248
- snapshot.cleanup();
249
- }
271
+ // Issue #1043 round 2: the durable clone IS the home. ensurePackClone
272
+ // already fast-forwarded it, so syncing is that pull, not a copy into
273
+ // a second location.
274
+ const home = (0, pack_home_1.resolvePackHome)('org');
275
+ const clone = (0, pack_home_1.ensurePackClone)(organization.gitUrl, home.root);
276
+ if (clone.sha === null)
277
+ throw new Error(clone.offlineReason ?? 'Pack clone unavailable.');
278
+ const metadata = {
279
+ version: clone.sha,
280
+ backend: 'git',
281
+ source: organization.gitUrl,
282
+ syncedAt: new Date().toISOString()
283
+ };
284
+ writeHomeMetadata(metadata);
285
+ return { status: 'synced', metadata };
250
286
  }
251
287
  const remoteUrl = options?.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
252
288
  const apiKey = options?.apiKey || (0, user_config_1.readUserFraimConfig)().apiKey;
@@ -259,7 +295,10 @@ async function syncOrgCache(options) {
259
295
  source: remoteUrl,
260
296
  syncedAt: new Date().toISOString()
261
297
  };
262
- materializeCache(pack.files, metadata);
298
+ // The cloud has no user-visible folder, so this managed directory is the
299
+ // home for that backend, not a cache beside one.
300
+ materializeCloudHome(pack.files, metadata);
301
+ writeHomeMetadata(metadata);
263
302
  return { status: 'synced', metadata };
264
303
  }
265
304
  catch (error) {
@@ -13,24 +13,22 @@ exports.publishOrgArtifacts = publishOrgArtifacts;
13
13
  * call: the read side is the synced `~/.fraim/org/` cache, the write side is
14
14
  * here. Supports both backends:
15
15
  * - fraim-cloud: POST /api/org/publish (authenticated by the user's API key)
16
- * - git: shallow-clone the org repo, write the files on a branch, push it,
17
- * and return the PR compare URL for the team to merge.
16
+ * - git: shallow-clone the org repo, apply the writes and deletions on a
17
+ * branch as one commit, push it, and open the pull request for the team.
18
18
  */
19
19
  const axios_1 = __importDefault(require("axios"));
20
- const child_process_1 = require("child_process");
21
- const fs_1 = __importDefault(require("fs"));
22
- const os_1 = __importDefault(require("os"));
23
20
  const path_1 = __importDefault(require("path"));
24
21
  const user_config_1 = require("./user-config");
25
22
  const local_folder_sync_1 = require("./local-folder-sync");
26
23
  const capability_pack_1 = require("../../core/capability-pack");
24
+ const pack_git_publish_1 = require("./pack-git-publish");
25
+ const pack_home_1 = require("./pack-home");
27
26
  const PACK_RELATIVE_PATH = /^(context|rules|learnings)\/[\w.-]+\.md$/;
28
27
  // #868: the company brand descriptor is JSON stored beside org_context.md. It
29
28
  // publishes through the same backends as org markdown but is delivered verbatim
30
29
  // (no managed-content header — that decoration is markdown-only, added on sync).
31
30
  // Mirrors org-pack-sync.isSafePackPath and fraim-mcp-server.isOrgPackPublishRelativePath.
32
31
  const ORG_BRAND_PACK_PATH = 'context/org_brand.json';
33
- const ALLOWED_GIT_URL = /^(https?:\/\/|ssh:\/\/|git:\/\/|file:\/\/|[\w.-]+@[\w.-]+:)/;
34
32
  /**
35
33
  * Infer the org-pack location for a local file by its name or path:
36
34
  * org_context.md -> context/org_context.md
@@ -56,12 +54,6 @@ function packRelativePathFor(filePath) {
56
54
  return `learnings/${base}`;
57
55
  throw new Error(`Cannot infer org-pack location for '${base}' (expected org_context.md, org_rules.md, org_brand.json, org-*.md, or a capability path like jobs/<cat>/<name>.md)`);
58
56
  }
59
- function gitCompareUrl(gitUrl, branch) {
60
- const httpUrl = gitUrl.replace(/\.git$/, '').replace(/^git@([^:]+):/, 'https://$1/');
61
- if (/^https?:\/\//.test(httpUrl))
62
- return `${httpUrl}/compare/${branch}?expand=1`;
63
- return undefined;
64
- }
65
57
  /**
66
58
  * Publish a set of org artifacts to the configured backend. Throws with a clear
67
59
  * message when no backend is configured, a path is outside the pack layout, or
@@ -74,13 +66,17 @@ async function publishOrgArtifacts(artifacts, opts) {
74
66
  }
75
67
  if (artifacts.length === 0)
76
68
  throw new Error('No artifacts to publish.');
69
+ const isValidOrgPackPath = (rel) => PACK_RELATIVE_PATH.test(rel) || rel === ORG_BRAND_PACK_PATH || (0, capability_pack_1.isCapabilityPackPath)(rel);
77
70
  for (const a of artifacts) {
78
- const isLegacyPath = PACK_RELATIVE_PATH.test(a.relativePath) || a.relativePath === ORG_BRAND_PACK_PATH;
79
- const isCapabilityPath = (0, capability_pack_1.isCapabilityPackPath)(a.relativePath);
80
- if (!isLegacyPath && !isCapabilityPath) {
81
- throw new Error(`Invalid org-pack path '${a.relativePath}' (must be context|rules|learnings/<name>.md, context/org_brand.json, or a capability path like jobs/<cat>/<name>.md).`);
71
+ if (!isValidOrgPackPath(a.relativePath)) {
72
+ throw new Error(`Invalid org-pack path '${a.relativePath}' (must be context|rules|learnings/<name>.md, context/org_brand.json, or a capability path like jobs/<cat>/<name>.md or employees/<slug>.json).`);
82
73
  }
83
74
  }
75
+ const deletions = opts?.deletions ?? [];
76
+ for (const rel of deletions) {
77
+ if (!isValidOrgPackPath(rel))
78
+ throw new Error(`Invalid org-pack deletion path '${rel}'.`);
79
+ }
84
80
  if (organization.backend === 'local-folder') {
85
81
  const localPath = organization.localPath;
86
82
  const writtenPaths = [];
@@ -91,7 +87,8 @@ async function publishOrgArtifacts(artifacts, opts) {
91
87
  }
92
88
  writtenPaths.push(path_1.default.join(localPath, a.relativePath));
93
89
  }
94
- return { backend: 'local-folder', writtenPaths };
90
+ const deletedPaths = (0, local_folder_sync_1.removeLocalFolderFiles)(localPath, deletions, artifacts.map((a) => a.relativePath));
91
+ return { backend: 'local-folder', writtenPaths, deletedPaths };
95
92
  }
96
93
  if (organization.backend === 'fraim-cloud') {
97
94
  const remoteUrl = opts?.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
@@ -102,27 +99,27 @@ async function publishOrgArtifacts(artifacts, opts) {
102
99
  return { backend: 'fraim-cloud', version: String(res.data?.version ?? '') };
103
100
  }
104
101
  // git backend
105
- const gitUrl = organization.gitUrl;
106
- if (!ALLOWED_GIT_URL.test(gitUrl))
107
- throw new Error(`Org repo URL has an unsupported scheme: ${gitUrl}`);
108
- const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'fraim-org-pub-'));
109
- try {
110
- (0, child_process_1.execFileSync)('git', ['clone', '--depth=1', '--quiet', '--', gitUrl, '.'], {
111
- cwd: dir, stdio: ['ignore', 'pipe', 'pipe'], timeout: 60_000
112
- });
113
- const branch = `fraim-org-update-${opts?.branchSuffix || `${process.pid}-${Date.now()}`}`;
114
- (0, child_process_1.execFileSync)('git', ['checkout', '-b', branch], { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'] });
115
- for (const a of artifacts) {
116
- const dest = path_1.default.join(dir, a.relativePath);
117
- fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
118
- fs_1.default.writeFileSync(dest, a.content);
119
- }
120
- (0, child_process_1.execFileSync)('git', ['add', '-A'], { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'] });
121
- (0, child_process_1.execFileSync)('git', ['commit', '-m', 'Update organization context'], { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'] });
122
- (0, child_process_1.execFileSync)('git', ['push', '--quiet', 'origin', branch], { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'], timeout: 60_000 });
123
- return { backend: 'git', branch, prUrl: gitCompareUrl(gitUrl, branch) };
124
- }
125
- finally {
126
- fs_1.default.rmSync(dir, { recursive: true, force: true });
127
- }
102
+ const change = (0, pack_git_publish_1.describePackChange)('organization', artifacts, deletions);
103
+ const published = await (0, pack_git_publish_1.publishPackToGitRepo)({
104
+ gitUrl: organization.gitUrl,
105
+ artifacts,
106
+ deletions,
107
+ branchSuffix: opts?.branchSuffix,
108
+ branchPrefix: 'fraim-org-update-',
109
+ commitMessage: change.title,
110
+ prTitle: change.title,
111
+ prBody: change.body,
112
+ openPullRequest: opts?.openPullRequest,
113
+ // Publish from the durable pack home, not a throwaway clone.
114
+ cloneDir: (0, pack_home_1.resolvePackHome)('org').root,
115
+ });
116
+ return {
117
+ backend: 'git',
118
+ branch: published.branch,
119
+ prUrl: published.prUrl,
120
+ prCreated: published.prCreated,
121
+ prBlockedReason: published.prBlockedReason,
122
+ credentialWarning: published.credentialWarning,
123
+ deletedPaths: deletions,
124
+ };
128
125
  }