fraim-framework 2.0.167 → 2.0.169

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 (33) hide show
  1. package/dist/src/ai-hub/catalog.js +28 -14
  2. package/dist/src/ai-hub/server.js +34 -406
  3. package/dist/src/cli/commands/init-project.js +1 -98
  4. package/dist/src/cli/commands/manager.js +40 -0
  5. package/dist/src/cli/commands/sync.js +17 -21
  6. package/dist/src/cli/fraim.js +2 -0
  7. package/dist/src/cli/utils/github-workflow-sync.js +12 -146
  8. package/dist/src/cli/utils/manager-pack-sync.js +188 -0
  9. package/dist/src/cli/utils/manager-publish.js +76 -0
  10. package/dist/src/cli/utils/user-config.js +20 -0
  11. package/dist/src/core/fraim-config-schema.generated.js +85 -10
  12. package/dist/src/core/manager-pack.js +26 -0
  13. package/dist/src/first-run/install-state.js +1 -0
  14. package/dist/src/first-run/server.js +9 -0
  15. package/dist/src/first-run/session-service.js +117 -23
  16. package/dist/src/first-run/types.js +2 -5
  17. package/dist/src/local-mcp-server/learning-context-builder.js +45 -8
  18. package/dist/src/local-mcp-server/stdio-server.js +28 -0
  19. package/package.json +1 -2
  20. package/public/ai-hub/index.html +0 -81
  21. package/public/ai-hub/script.js +3 -219
  22. package/public/ai-hub/styles.css +8 -36
  23. package/public/first-run/index.html +1 -1
  24. package/public/first-run/script.js +459 -530
  25. package/public/first-run/styles.css +288 -73
  26. package/dist/src/config/ai-manager-hiring.js +0 -121
  27. package/dist/src/config/compat.js +0 -16
  28. package/dist/src/config/feature-flags.js +0 -25
  29. package/dist/src/config/persona-capability-bundles.js +0 -273
  30. package/dist/src/config/persona-hiring.js +0 -270
  31. package/dist/src/config/portfolio-slug-overrides.js +0 -17
  32. package/dist/src/config/pricing.js +0 -37
  33. package/dist/src/config/stripe.js +0 -43
@@ -3,13 +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.initProjectCommand = exports.runInitProject = exports.findProjectFile = void 0;
6
+ exports.initProjectCommand = exports.runInitProject = void 0;
7
7
  const commander_1 = require("commander");
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const os_1 = __importDefault(require("os"));
11
11
  const chalk_1 = __importDefault(require("chalk"));
12
- const child_process_1 = require("child_process");
13
12
  const sync_1 = require("./sync");
14
13
  const platform_detection_1 = require("../utils/platform-detection");
15
14
  const ide_detector_1 = require("../setup/ide-detector");
@@ -19,8 +18,6 @@ const fraim_gitignore_1 = require("../utils/fraim-gitignore");
19
18
  const agent_adapters_1 = require("../utils/agent-adapters");
20
19
  const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
21
20
  const project_bootstrap_1 = require("../utils/project-bootstrap");
22
- const config_loader_1 = require("../../core/config-loader");
23
- const github_workflow_sync_1 = require("../utils/github-workflow-sync");
24
21
  const checkGlobalSetup = () => {
25
22
  const fraimUserDir = process.env.FRAIM_USER_DIR || path_1.default.join(os_1.default.homedir(), '.fraim');
26
23
  const globalConfigPath = path_1.default.join(fraimUserDir, 'config.json');
@@ -40,93 +37,6 @@ const checkGlobalSetup = () => {
40
37
  return { exists: true, mode: 'integrated', tokens: {} };
41
38
  }
42
39
  };
43
- // Robust path resolution utility - walks up directory tree to find target
44
- const findProjectFile = (filename) => {
45
- let currentDir = __dirname;
46
- // Walk up the directory tree to find the target file/directory
47
- for (let i = 0; i < 10; i++) { // Limit to prevent infinite loop
48
- const targetPath = path_1.default.join(currentDir, filename);
49
- if (fs_1.default.existsSync(targetPath)) {
50
- return targetPath;
51
- }
52
- const parentDir = path_1.default.dirname(currentDir);
53
- if (parentDir === currentDir)
54
- break; // Reached root
55
- currentDir = parentDir;
56
- }
57
- // Fallback: try from process.cwd()
58
- const cwdTarget = path_1.default.join(process.cwd(), filename);
59
- if (fs_1.default.existsSync(cwdTarget)) {
60
- return cwdTarget;
61
- }
62
- // Last resort: use relative path from __dirname
63
- return path_1.default.join(__dirname, '..', '..', '..', filename);
64
- };
65
- exports.findProjectFile = findProjectFile;
66
- ;
67
- const installGitHubWorkflows = (projectRoot) => {
68
- const registryDir = (0, exports.findProjectFile)('registry');
69
- const sourceDir = path_1.default.join(registryDir, 'github', 'workflows');
70
- if (!fs_1.default.existsSync(sourceDir)) {
71
- console.log(chalk_1.default.yellow(`Warning: GitHub workflows source directory not found: ${sourceDir}`));
72
- console.log(chalk_1.default.gray(`Registry directory: ${registryDir}`));
73
- console.log(chalk_1.default.gray(`Current __dirname: ${__dirname}`));
74
- console.log(chalk_1.default.gray(`Process cwd: ${process.cwd()}`));
75
- return;
76
- }
77
- const config = fs_1.default.existsSync((0, project_fraim_paths_1.getWorkspaceConfigPath)(projectRoot))
78
- ? (0, config_loader_1.loadFraimConfig)((0, project_fraim_paths_1.getWorkspaceConfigPath)(projectRoot))
79
- : null;
80
- if (!(0, github_workflow_sync_1.isGitHubWorkflowAutomationEnabled)(config)) {
81
- console.log(chalk_1.default.gray('GitHub workflow automation disabled or not configured; skipping managed workflow install.'));
82
- return;
83
- }
84
- const bundle = (0, github_workflow_sync_1.loadLocalGitHubWorkflowBundle)(registryDir);
85
- const result = (0, github_workflow_sync_1.reconcileGitHubWorkflowAssets)({
86
- projectRoot,
87
- files: bundle,
88
- config
89
- });
90
- for (const line of (0, github_workflow_sync_1.formatGitHubWorkflowSyncSummary)(result)) {
91
- console.log(chalk_1.default.green(line));
92
- }
93
- };
94
- const createGitHubLabels = (projectRoot) => {
95
- try {
96
- (0, child_process_1.execSync)('gh --version', { stdio: 'ignore' });
97
- }
98
- catch {
99
- console.log(chalk_1.default.yellow('GitHub CLI (gh) not found. Skipping label creation.'));
100
- console.log(chalk_1.default.gray('Install gh CLI to enable automatic label creation: https://cli.github.com/'));
101
- return;
102
- }
103
- const labelsPath = (0, exports.findProjectFile)('labels.json');
104
- if (!fs_1.default.existsSync(labelsPath)) {
105
- console.log(chalk_1.default.yellow('labels.json not found. Skipping label creation.'));
106
- console.log(chalk_1.default.gray(`Searched from: ${__dirname}`));
107
- return;
108
- }
109
- try {
110
- const labels = JSON.parse(fs_1.default.readFileSync(labelsPath, 'utf8'));
111
- labels.forEach((label) => {
112
- try {
113
- (0, child_process_1.execSync)(`gh label create "${label.name}" --color "${label.color}" --description "${label.description}"`, { cwd: projectRoot, stdio: 'ignore' });
114
- console.log(chalk_1.default.green(`Created label: ${label.name}`));
115
- }
116
- catch (error) {
117
- if (error.message && error.message.includes('already exists')) {
118
- console.log(chalk_1.default.gray(`Label already exists: ${label.name}`));
119
- }
120
- else {
121
- console.log(chalk_1.default.yellow(`Could not create label ${label.name}: ${error.message}`));
122
- }
123
- }
124
- });
125
- }
126
- catch (error) {
127
- console.log(chalk_1.default.yellow(`Error reading labels.json: ${error.message}`));
128
- }
129
- };
130
40
  function formatPlatformLabel(provider) {
131
41
  switch (provider) {
132
42
  case 'ado':
@@ -261,13 +171,6 @@ const runInitProject = async (options = {}) => {
261
171
  if (ignoreUpdate.gitignoreUpdated && !isMinimalConversationMode(result.mode)) {
262
172
  console.log(chalk_1.default.green('Removed legacy FRAIM sync block from .gitignore'));
263
173
  }
264
- const detection = (0, platform_detection_1.detectPlatformFromGit)();
265
- if (detection.provider === 'github' && !isMinimalConversationMode(result.mode)) {
266
- console.log(chalk_1.default.blue('\nSetting up GitHub labels...'));
267
- installGitHubWorkflows(projectRoot);
268
- createGitHubLabels(projectRoot);
269
- result.repositoryDetected = true;
270
- }
271
174
  if (!process.env.FRAIM_SKIP_SYNC) {
272
175
  await (0, sync_1.runSync)({ projectRoot, failHard });
273
176
  result.syncPerformed = true;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.managerCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const manager_publish_1 = require("../utils/manager-publish");
11
+ exports.managerCommand = new commander_1.Command('manager')
12
+ .description('Manage your portable personal manager context');
13
+ exports.managerCommand
14
+ .command('publish')
15
+ .description('Publish manager context/rules/learnings files to the configured manager backend')
16
+ .argument('<files...>', 'Local files to publish: manager_context.md, manager_rules.md, and/or personal learning files')
17
+ .action(async (files) => {
18
+ try {
19
+ const artifacts = files.map((filePath) => {
20
+ if (!fs_1.default.existsSync(filePath))
21
+ throw new Error(`File not found: ${filePath}`);
22
+ return { relativePath: (0, manager_publish_1.managerPackRelativePathFor)(filePath), content: fs_1.default.readFileSync(filePath, 'utf8') };
23
+ });
24
+ const result = await (0, manager_publish_1.publishManagerArtifacts)(artifacts);
25
+ if (result.backend === 'fraim-cloud') {
26
+ console.log(chalk_1.default.green(`Published ${artifacts.length} manager artifact(s) to your FRAIM account (version ${result.version}).`));
27
+ console.log(chalk_1.default.gray('Run "fraim sync" on each machine to pull the update.'));
28
+ }
29
+ else {
30
+ console.log(chalk_1.default.green(`Pushed ${artifacts.length} manager artifact(s) to branch "${result.branch}" in your manager repo.`));
31
+ if (result.prUrl)
32
+ console.log(chalk_1.default.cyan(`Open the pull request: ${result.prUrl}`));
33
+ console.log(chalk_1.default.gray('After it merges, run "fraim sync" to pull the update.'));
34
+ }
35
+ }
36
+ catch (error) {
37
+ console.error(chalk_1.default.red(`manager publish failed: ${error.message}`));
38
+ process.exit(1);
39
+ }
40
+ });
@@ -48,7 +48,6 @@ const git_utils_1 = require("../../core/utils/git-utils");
48
48
  const agent_adapters_1 = require("../utils/agent-adapters");
49
49
  const fraim_gitignore_1 = require("../utils/fraim-gitignore");
50
50
  const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
51
- const github_workflow_sync_1 = require("../utils/github-workflow-sync");
52
51
  function resolveExplicitLocalSyncUrl() {
53
52
  const candidates = [
54
53
  process.env.FRAIM_TEST_SERVER_URL,
@@ -180,6 +179,21 @@ const runSync = async (options) => {
180
179
  }
181
180
  }
182
181
  };
182
+ const refreshManagerCache = async (remoteUrl, apiKey) => {
183
+ if (process.env.TEST_MODE === 'true')
184
+ return;
185
+ const { syncManagerCache } = await Promise.resolve().then(() => __importStar(require('../utils/manager-pack-sync')));
186
+ const outcome = await syncManagerCache({ remoteUrl, apiKey });
187
+ if (outcome.status === 'synced') {
188
+ console.log(chalk_1.default.green(`Manager context synced (${outcome.metadata.backend}, version ${outcome.metadata.version.slice(0, 12)})`));
189
+ }
190
+ else if (outcome.status === 'stale') {
191
+ console.log(chalk_1.default.yellow(`Manager source unreachable. Using cached manager context from ${Math.round(outcome.ageHours)}h ago. Will refresh when reachable.`));
192
+ }
193
+ else if (outcome.status === 'absent') {
194
+ console.log(chalk_1.default.yellow(`Manager context not synced: ${outcome.error}`));
195
+ }
196
+ };
183
197
  const isNpx = process.env.npm_config_prefix === undefined || process.env.npm_lifecycle_event === 'npx';
184
198
  const isGlobal = !isNpx && (process.env.npm_config_global === 'true' || process.env.npm_config_prefix);
185
199
  if (isGlobal && !options.skipUpdates) {
@@ -214,17 +228,8 @@ const runSync = async (options) => {
214
228
  if (adapterUpdates.length > 0) {
215
229
  console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${adapterUpdates.join(', ')}`));
216
230
  }
217
- if (config.repository?.provider === 'github' && (0, github_workflow_sync_1.isGitHubWorkflowAutomationEnabled)(config)) {
218
- const workflowBundle = await (0, github_workflow_sync_1.fetchGitHubWorkflowBundle)({
219
- remoteUrl: localUrl,
220
- apiKey: 'local-dev'
221
- });
222
- const workflowResult = (0, github_workflow_sync_1.reconcileGitHubWorkflowAssets)({ projectRoot, files: workflowBundle, config });
223
- for (const line of (0, github_workflow_sync_1.formatGitHubWorkflowSyncSummary)(workflowResult)) {
224
- console.log(chalk_1.default.green(line));
225
- }
226
- }
227
231
  await refreshOrgCache(localUrl, 'local-dev');
232
+ await refreshManagerCache(localUrl, 'local-dev');
228
233
  return;
229
234
  }
230
235
  console.error(chalk_1.default.red(`Local sync failed: ${result.error}`));
@@ -264,21 +269,12 @@ const runSync = async (options) => {
264
269
  removeLegacyVersionFromConfig(fraimDir);
265
270
  writeSyncMetadata(fraimDir, 'remote', config.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me');
266
271
  refreshLocalIgnoreConfig();
267
- if (config.repository?.provider === 'github' && (0, github_workflow_sync_1.isGitHubWorkflowAutomationEnabled)(config)) {
268
- const workflowBundle = await (0, github_workflow_sync_1.fetchGitHubWorkflowBundle)({
269
- remoteUrl: config.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me',
270
- apiKey
271
- });
272
- const workflowResult = (0, github_workflow_sync_1.reconcileGitHubWorkflowAssets)({ projectRoot, files: workflowBundle, config });
273
- for (const line of (0, github_workflow_sync_1.formatGitHubWorkflowSyncSummary)(workflowResult)) {
274
- console.log(chalk_1.default.green(line));
275
- }
276
- }
277
272
  const adapterUpdates = (0, agent_adapters_1.ensureAgentAdapterFiles)(projectRoot);
278
273
  if (adapterUpdates.length > 0) {
279
274
  console.log(chalk_1.default.green(`Updated FRAIM agent adapter files: ${adapterUpdates.join(', ')}`));
280
275
  }
281
276
  await refreshOrgCache(config.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me', apiKey);
277
+ await refreshManagerCache(config.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me', apiKey);
282
278
  };
283
279
  exports.runSync = runSync;
284
280
  exports.syncCommand = new commander_1.Command('sync')
@@ -54,6 +54,7 @@ const hub_1 = require("./commands/hub");
54
54
  const first_run_1 = require("./commands/first-run");
55
55
  const workspace_config_1 = require("./commands/workspace-config");
56
56
  const org_1 = require("./commands/org");
57
+ const manager_1 = require("./commands/manager");
57
58
  const fs_1 = __importDefault(require("fs"));
58
59
  const path_1 = __importDefault(require("path"));
59
60
  const program = new commander_1.Command();
@@ -97,6 +98,7 @@ program.addCommand(hub_1.hubCommand);
97
98
  program.addCommand(first_run_1.firstRunCommand);
98
99
  program.addCommand(workspace_config_1.workspaceConfigCommand);
99
100
  program.addCommand(org_1.orgCommand);
101
+ program.addCommand(manager_1.managerCommand);
100
102
  // Wait for async command initialization before parsing
101
103
  (async () => {
102
104
  // Import the initialization promise from setup command
@@ -3,67 +3,15 @@ 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.GITHUB_WORKFLOW_ASSET_PREFIX = exports.GITHUB_WORKFLOW_HEADER = exports.GITHUB_WORKFLOW_MANIFEST_RELATIVE_PATH = void 0;
7
- exports.isGitHubWorkflowAutomationEnabled = isGitHubWorkflowAutomationEnabled;
8
- exports.decorateManagedGitHubWorkflow = decorateManagedGitHubWorkflow;
9
- exports.isFraimManagedGitHubWorkflow = isFraimManagedGitHubWorkflow;
10
- exports.loadGitHubWorkflowManifest = loadGitHubWorkflowManifest;
11
6
  exports.loadLocalGitHubWorkflowBundle = loadLocalGitHubWorkflowBundle;
12
7
  exports.fetchGitHubWorkflowBundle = fetchGitHubWorkflowBundle;
13
- exports.reconcileGitHubWorkflowAssets = reconcileGitHubWorkflowAssets;
8
+ exports.installGitHubWorkflowAssets = installGitHubWorkflowAssets;
14
9
  exports.formatGitHubWorkflowSyncSummary = formatGitHubWorkflowSyncSummary;
15
10
  const axios_1 = __importDefault(require("axios"));
16
- const crypto_1 = __importDefault(require("crypto"));
17
11
  const fs_1 = __importDefault(require("fs"));
18
12
  const path_1 = __importDefault(require("path"));
19
13
  const config_loader_1 = require("../../core/config-loader");
20
14
  const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
21
- exports.GITHUB_WORKFLOW_MANIFEST_RELATIVE_PATH = path_1.default.join('fraim', 'managed-assets', 'github-workflows.json');
22
- exports.GITHUB_WORKFLOW_HEADER = '# FRAIM-MANAGED: github-workflow';
23
- exports.GITHUB_WORKFLOW_ASSET_PREFIX = '# FRAIM-ASSET-ID: ';
24
- function sha256(content) {
25
- return `sha256:${crypto_1.default.createHash('sha256').update(content).digest('hex')}`;
26
- }
27
- function getManifestPath(projectRoot) {
28
- return path_1.default.join(projectRoot, exports.GITHUB_WORKFLOW_MANIFEST_RELATIVE_PATH);
29
- }
30
- function isGitHubWorkflowAutomationEnabled(config) {
31
- return config?.customizations?.githubWorkflows?.enabled === true;
32
- }
33
- function decorateManagedGitHubWorkflow(assetId, content) {
34
- const normalized = content.replace(/^\uFEFF/, '');
35
- const assetHeader = `${exports.GITHUB_WORKFLOW_HEADER}\n${exports.GITHUB_WORKFLOW_ASSET_PREFIX}${assetId}`;
36
- if (normalized.includes(exports.GITHUB_WORKFLOW_HEADER) && normalized.includes(`${exports.GITHUB_WORKFLOW_ASSET_PREFIX}${assetId}`)) {
37
- return normalized;
38
- }
39
- return `${assetHeader}\n${normalized}`;
40
- }
41
- function isFraimManagedGitHubWorkflow(content, assetId) {
42
- if (!content.includes(exports.GITHUB_WORKFLOW_HEADER)) {
43
- return false;
44
- }
45
- if (!assetId) {
46
- return true;
47
- }
48
- return content.includes(`${exports.GITHUB_WORKFLOW_ASSET_PREFIX}${assetId}`);
49
- }
50
- function loadGitHubWorkflowManifest(projectRoot) {
51
- const manifestPath = getManifestPath(projectRoot);
52
- if (!fs_1.default.existsSync(manifestPath)) {
53
- return {
54
- version: 1,
55
- enabled: true,
56
- provider: 'github',
57
- managedFiles: []
58
- };
59
- }
60
- return JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf8'));
61
- }
62
- function writeGitHubWorkflowManifest(projectRoot, manifest) {
63
- const manifestPath = getManifestPath(projectRoot);
64
- fs_1.default.mkdirSync(path_1.default.dirname(manifestPath), { recursive: true });
65
- fs_1.default.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
66
- }
67
15
  function loadLocalGitHubWorkflowBundle(registryRoot) {
68
16
  const workflowsRoot = path_1.default.join(registryRoot, 'github', 'workflows');
69
17
  if (!fs_1.default.existsSync(workflowsRoot)) {
@@ -77,8 +25,7 @@ function loadLocalGitHubWorkflowBundle(registryRoot) {
77
25
  return {
78
26
  path: entry.name,
79
27
  content,
80
- type: 'github-workflow',
81
- digest: sha256(content)
28
+ type: 'github-workflow'
82
29
  };
83
30
  });
84
31
  return files.sort((a, b) => a.path.localeCompare(b.path));
@@ -98,7 +45,7 @@ function readProjectConfig(projectRoot, config) {
98
45
  }
99
46
  return (0, config_loader_1.loadFraimConfig)((0, project_fraim_paths_1.getWorkspaceConfigPath)(projectRoot));
100
47
  }
101
- function reconcileGitHubWorkflowAssets(options) {
48
+ function installGitHubWorkflowAssets(options) {
102
49
  const config = readProjectConfig(options.projectRoot, options.config);
103
50
  const provider = config.repository?.provider || null;
104
51
  if (provider !== 'github') {
@@ -109,108 +56,27 @@ function reconcileGitHubWorkflowAssets(options) {
109
56
  results: []
110
57
  };
111
58
  }
112
- if (!isGitHubWorkflowAutomationEnabled(config)) {
113
- return {
114
- enabled: false,
115
- provider,
116
- skippedReason: 'GitHub workflow automation disabled',
117
- results: []
118
- };
119
- }
120
- const manifest = loadGitHubWorkflowManifest(options.projectRoot);
121
- const manifestEntries = new Map(manifest.managedFiles.map((entry) => [entry.path, entry]));
122
59
  const workflowsDir = path_1.default.join(options.projectRoot, '.github', 'workflows');
123
60
  fs_1.default.mkdirSync(workflowsDir, { recursive: true });
124
61
  const results = [];
125
- const nextManagedFiles = [];
126
62
  for (const file of options.files) {
127
- const assetId = path_1.default.basename(file.path);
128
63
  const relativeWorkflowPath = path_1.default.join('.github', 'workflows', file.path).replace(/[\\/]/g, '/');
129
- const destinationPath = path_1.default.join(options.projectRoot, '.github', 'workflows', file.path);
130
- const desiredContent = decorateManagedGitHubWorkflow(assetId, file.content);
131
- const desiredInstalledDigest = sha256(desiredContent);
132
- const previousEntry = manifestEntries.get(relativeWorkflowPath);
133
- if (!fs_1.default.existsSync(destinationPath)) {
134
- fs_1.default.mkdirSync(path_1.default.dirname(destinationPath), { recursive: true });
135
- fs_1.default.writeFileSync(destinationPath, desiredContent, 'utf8');
136
- nextManagedFiles.push({
137
- path: relativeWorkflowPath,
138
- assetId,
139
- sourceDigest: file.digest,
140
- installedDigest: desiredInstalledDigest
141
- });
142
- results.push({
143
- path: relativeWorkflowPath,
144
- assetId,
145
- status: 'installed'
146
- });
147
- continue;
148
- }
149
- const currentContent = fs_1.default.readFileSync(destinationPath, 'utf8');
150
- const currentDigest = sha256(currentContent);
151
- const ownedByFraim = isFraimManagedGitHubWorkflow(currentContent, assetId) || Boolean(previousEntry);
152
- if (!ownedByFraim) {
153
- if (previousEntry) {
154
- nextManagedFiles.push(previousEntry);
155
- }
156
- results.push({
157
- path: relativeWorkflowPath,
158
- assetId,
159
- status: 'conflict',
160
- reason: 'same-named file exists without FRAIM ownership marker'
161
- });
162
- continue;
163
- }
164
- if (currentDigest === desiredInstalledDigest) {
165
- nextManagedFiles.push({
166
- path: relativeWorkflowPath,
167
- assetId,
168
- sourceDigest: file.digest,
169
- installedDigest: desiredInstalledDigest
170
- });
64
+ const destinationPath = path_1.default.join(workflowsDir, file.path);
65
+ if (fs_1.default.existsSync(destinationPath)) {
171
66
  results.push({
172
67
  path: relativeWorkflowPath,
173
- assetId,
174
- status: 'already current'
68
+ status: 'skipped',
69
+ reason: 'already exists'
175
70
  });
176
71
  continue;
177
72
  }
178
- if (previousEntry && currentDigest === previousEntry.installedDigest) {
179
- fs_1.default.writeFileSync(destinationPath, desiredContent, 'utf8');
180
- nextManagedFiles.push({
181
- path: relativeWorkflowPath,
182
- assetId,
183
- sourceDigest: file.digest,
184
- installedDigest: desiredInstalledDigest
185
- });
186
- results.push({
187
- path: relativeWorkflowPath,
188
- assetId,
189
- status: 'updated'
190
- });
191
- continue;
192
- }
193
- nextManagedFiles.push(previousEntry || {
194
- path: relativeWorkflowPath,
195
- assetId,
196
- sourceDigest: file.digest,
197
- installedDigest: currentDigest
198
- });
73
+ fs_1.default.mkdirSync(path_1.default.dirname(destinationPath), { recursive: true });
74
+ fs_1.default.writeFileSync(destinationPath, file.content, 'utf8');
199
75
  results.push({
200
76
  path: relativeWorkflowPath,
201
- assetId,
202
- status: 'conflict',
203
- reason: previousEntry
204
- ? 'managed file has local edits that do not match the last installed digest'
205
- : 'FRAIM-managed header exists without manifest baseline'
77
+ status: 'installed'
206
78
  });
207
79
  }
208
- writeGitHubWorkflowManifest(options.projectRoot, {
209
- version: 1,
210
- enabled: true,
211
- provider: 'github',
212
- managedFiles: nextManagedFiles.sort((a, b) => a.path.localeCompare(b.path))
213
- });
214
80
  return {
215
81
  enabled: true,
216
82
  provider,
@@ -219,10 +85,10 @@ function reconcileGitHubWorkflowAssets(options) {
219
85
  }
220
86
  function formatGitHubWorkflowSyncSummary(result) {
221
87
  if (!result.enabled) {
222
- return [`GitHub workflow reconciliation skipped: ${result.skippedReason || 'not enabled'}`];
88
+ return [`GitHub workflow install skipped: ${result.skippedReason || 'not enabled'}`];
223
89
  }
224
90
  if (result.results.length === 0) {
225
- return ['GitHub workflow reconciliation: no workflow assets found'];
91
+ return ['GitHub workflow install: no workflow assets found'];
226
92
  }
227
93
  return result.results.map((item) => {
228
94
  const suffix = item.reason ? ` (${item.reason})` : '';
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MANAGER_CACHE_MANAGED_HEADER = exports.MANAGER_SYNC_METADATA_FILE = exports.MANAGER_CACHE_DIRNAME = void 0;
7
+ exports.getManagerCacheDir = getManagerCacheDir;
8
+ exports.readManagerCacheMetadata = readManagerCacheMetadata;
9
+ exports.getManagerCacheAgeHours = getManagerCacheAgeHours;
10
+ exports.syncManagerCache = syncManagerCache;
11
+ /**
12
+ * Manager cache sync (issue #580).
13
+ *
14
+ * Materializes the manager's personal context/rules/learnings into the managed
15
+ * cache at ~/.fraim/manager/, from either backend:
16
+ * - git: shallow snapshot of a manager-owned repo
17
+ * - fraim-cloud: GET /api/manager/pack from the FRAIM server
18
+ *
19
+ * Cache files are managed content: marked, overwritten on every sync, and
20
+ * never a direct write target for agents.
21
+ */
22
+ const axios_1 = __importDefault(require("axios"));
23
+ const fs_1 = __importDefault(require("fs"));
24
+ const path_1 = __importDefault(require("path"));
25
+ const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
26
+ const manager_pack_1 = require("../../core/manager-pack");
27
+ const git_org_sync_1 = require("./git-org-sync");
28
+ const user_config_1 = require("./user-config");
29
+ exports.MANAGER_CACHE_DIRNAME = 'manager';
30
+ exports.MANAGER_SYNC_METADATA_FILE = '.manager-sync-metadata.json';
31
+ exports.MANAGER_CACHE_MANAGED_HEADER = '<!-- FRAIM_MANAGER_SYNC_MANAGED_CONTENT -->';
32
+ const MANAGER_PACK_DIRS = ['context', 'rules', 'learnings'];
33
+ function getManagerCacheDir() {
34
+ return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), exports.MANAGER_CACHE_DIRNAME);
35
+ }
36
+ function readManagerCacheMetadata() {
37
+ try {
38
+ const metadataPath = path_1.default.join(getManagerCacheDir(), exports.MANAGER_SYNC_METADATA_FILE);
39
+ if (!fs_1.default.existsSync(metadataPath))
40
+ return null;
41
+ return JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf8'));
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ function getManagerCacheAgeHours() {
48
+ const metadata = readManagerCacheMetadata();
49
+ if (!metadata?.syncedAt)
50
+ return null;
51
+ const syncedAt = Date.parse(metadata.syncedAt);
52
+ if (Number.isNaN(syncedAt))
53
+ return null;
54
+ return Math.max(0, (Date.now() - syncedAt) / 3_600_000);
55
+ }
56
+ function decorateManagedManagerFile(content, backend) {
57
+ const normalized = content.replace(/^\uFEFF/, '');
58
+ if (normalized.startsWith(exports.MANAGER_CACHE_MANAGED_HEADER))
59
+ return normalized;
60
+ const writePath = backend === 'git'
61
+ ? 'open a pull request against your manager storage repo'
62
+ : 'publish the change through your FRAIM manager storage flow';
63
+ const marker = [
64
+ exports.MANAGER_CACHE_MANAGED_HEADER,
65
+ '> [!IMPORTANT]',
66
+ '> Synced from your personal manager storage. Local edits are overwritten on the next sync.',
67
+ `> To change this content, ${writePath}.`,
68
+ ''
69
+ ].join('\n');
70
+ return `${marker}\n${normalized}`;
71
+ }
72
+ function collectGitPackFiles(snapshotDir) {
73
+ const files = [];
74
+ for (const dirName of MANAGER_PACK_DIRS) {
75
+ const dirPath = path_1.default.join(snapshotDir, dirName);
76
+ if (!fs_1.default.existsSync(dirPath))
77
+ continue;
78
+ for (const entry of fs_1.default.readdirSync(dirPath, { withFileTypes: true })) {
79
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
80
+ continue;
81
+ files.push({
82
+ relativePath: `${dirName}/${entry.name}`,
83
+ content: fs_1.default.readFileSync(path_1.default.join(dirPath, entry.name), 'utf8')
84
+ });
85
+ }
86
+ }
87
+ return files;
88
+ }
89
+ async function fetchCloudPack(remoteUrl, apiKey) {
90
+ const response = await axios_1.default.get(`${remoteUrl.replace(/\/$/, '')}/api/manager/pack`, {
91
+ headers: { 'x-api-key': apiKey },
92
+ timeout: 30_000
93
+ });
94
+ const files = Array.isArray(response.data?.files) ? response.data.files : [];
95
+ return {
96
+ files: files.filter((f) => typeof f?.relativePath === 'string' &&
97
+ (0, manager_pack_1.isManagerPackRelativePath)(f.relativePath) &&
98
+ typeof f?.content === 'string'),
99
+ version: String(response.data?.version ?? '0')
100
+ };
101
+ }
102
+ function materializeCache(files, metadata) {
103
+ const cacheDir = getManagerCacheDir();
104
+ const stagingDir = `${cacheDir}.staging-${process.pid}`;
105
+ fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
106
+ fs_1.default.mkdirSync(stagingDir, { recursive: true });
107
+ for (const file of files) {
108
+ if (!(0, manager_pack_1.isManagerPackRelativePath)(file.relativePath))
109
+ continue;
110
+ const destination = path_1.default.join(stagingDir, file.relativePath);
111
+ fs_1.default.mkdirSync(path_1.default.dirname(destination), { recursive: true });
112
+ fs_1.default.writeFileSync(destination, decorateManagedManagerFile(file.content, metadata.backend));
113
+ }
114
+ fs_1.default.writeFileSync(path_1.default.join(stagingDir, exports.MANAGER_SYNC_METADATA_FILE), JSON.stringify(metadata, null, 2));
115
+ fs_1.default.rmSync(cacheDir, { recursive: true, force: true });
116
+ fs_1.default.renameSync(stagingDir, cacheDir);
117
+ }
118
+ async function discoverCloudManagerStorage(options) {
119
+ try {
120
+ const apiKey = options?.apiKey || (0, user_config_1.readUserFraimConfig)().apiKey;
121
+ if (!apiKey || apiKey === 'local-dev' || apiKey === 'test-mode-key')
122
+ return null;
123
+ const remoteUrl = options?.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
124
+ const pack = await fetchCloudPack(remoteUrl, apiKey);
125
+ if (pack.files.length === 0)
126
+ return null;
127
+ (0, user_config_1.writeUserFraimConfig)({ managerStorage: { backend: 'fraim-cloud' } });
128
+ return (0, user_config_1.getManagerStorageConfig)();
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ }
134
+ function failureOutcome(error) {
135
+ const message = error instanceof Error ? error.message : String(error);
136
+ const existing = readManagerCacheMetadata();
137
+ if (existing) {
138
+ return {
139
+ status: 'stale',
140
+ metadata: existing,
141
+ ageHours: getManagerCacheAgeHours() ?? 0,
142
+ error: message
143
+ };
144
+ }
145
+ return { status: 'absent', error: message };
146
+ }
147
+ async function syncManagerCache(options) {
148
+ let managerStorage = (0, user_config_1.getManagerStorageConfig)();
149
+ if (!managerStorage) {
150
+ managerStorage = await discoverCloudManagerStorage(options);
151
+ if (!managerStorage)
152
+ return { status: 'disabled' };
153
+ }
154
+ try {
155
+ if (managerStorage.backend === 'git') {
156
+ const snapshot = (0, git_org_sync_1.fetchOrgRepoSnapshot)(managerStorage.gitUrl);
157
+ try {
158
+ const metadata = {
159
+ version: snapshot.sha,
160
+ backend: 'git',
161
+ source: managerStorage.gitUrl,
162
+ syncedAt: new Date().toISOString()
163
+ };
164
+ materializeCache(collectGitPackFiles(snapshot.dir), metadata);
165
+ return { status: 'synced', metadata };
166
+ }
167
+ finally {
168
+ snapshot.cleanup();
169
+ }
170
+ }
171
+ const remoteUrl = options?.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
172
+ const apiKey = options?.apiKey || (0, user_config_1.readUserFraimConfig)().apiKey;
173
+ if (!apiKey)
174
+ throw new Error('No FRAIM API key available for the fraim-cloud manager backend.');
175
+ const pack = await fetchCloudPack(remoteUrl, apiKey);
176
+ const metadata = {
177
+ version: pack.version,
178
+ backend: 'fraim-cloud',
179
+ source: remoteUrl,
180
+ syncedAt: new Date().toISOString()
181
+ };
182
+ materializeCache(pack.files, metadata);
183
+ return { status: 'synced', metadata };
184
+ }
185
+ catch (error) {
186
+ return failureOutcome(error);
187
+ }
188
+ }