backend-manager 5.9.1 → 5.9.2

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/CHANGELOG.md CHANGED
@@ -14,6 +14,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
14
14
  - `Fixed` for any bug fixes.
15
15
  - `Security` in case of vulnerabilities.
16
16
 
17
+ # [5.9.2] - 2026-06-20
18
+
19
+ ### Added
20
+ - Blog generation extended test (`test/content/blog-generate.js`). Run `npx mgr test mgr:content/blog-generate --extended` to exercise the full blog auto-publisher pipeline (source resolution → Ghostii → publish). Supports `BLOG_SOURCE`, `BLOG_NO_PUBLISH`, `BLOG_OPEN` env vars.
21
+
17
22
  # [5.9.1] - 2026-06-20
18
23
 
19
24
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.9.1",
3
+ "version": "5.9.2",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Blog article generation test.
3
+ *
4
+ * Two modes:
5
+ *
6
+ * 1. DEFAULT MODE (no env required)
7
+ * Verifies the blog auto-publisher config is readable and source resolution
8
+ * works correctly. No AI calls, no publishing. Fast, free, deterministic.
9
+ *
10
+ * 2. AI PIPELINE MODE (set TEST_EXTENDED_MODE=1)
11
+ * Runs the full blog auto-publisher pipeline: resolves a source, calls
12
+ * Ghostii to write an article, and publishes it to the website repo via
13
+ * admin/post. Same code path the daily cron uses. Costs money (AI tokens).
14
+ *
15
+ * Run from any consumer project's functions/ directory:
16
+ * npx mgr test mgr:content/blog-generate # config check (fast, free)
17
+ * TEST_EXTENDED_MODE=1 npx mgr test mgr:content/blog-generate # full AI pipeline
18
+ *
19
+ * --- Env vars (AI mode only, require TEST_EXTENDED_MODE=1) ---
20
+ * BLOG_SOURCE=<type> Override the source type for this run.
21
+ * Values: '$brand', '$parent', '$feed:<url>', a URL, or text.
22
+ * Default: picks randomly from the first content entry's sources[].
23
+ * BLOG_NO_PUBLISH=1 Generate the article but do NOT publish to the website repo.
24
+ * Useful for iterating on prompt quality without committing posts.
25
+ * BLOG_OPEN=1 Auto-open the generated article in the default browser (macOS only).
26
+ *
27
+ * AI mode requires:
28
+ * BACKEND_MANAGER_KEY — authenticates with Ghostii + parent server
29
+ * OPENAI_API_KEY — Ghostii uses OpenAI internally (or BACKEND_MANAGER_OPENAI_API_KEY)
30
+ * PARENT_API_URL — or set `parent` in backend-manager-config.json (for $parent sources)
31
+ *
32
+ * Default mode requires: nothing.
33
+ */
34
+ const path = require('path');
35
+ const { execSync } = require('child_process');
36
+ const jetpack = require('fs-jetpack');
37
+ const powertools = require('node-powertools');
38
+
39
+ module.exports = {
40
+ description: 'Generate a blog article (config check by default, full AI pipeline with TEST_EXTENDED_MODE)',
41
+ auth: 'none',
42
+ timeout: 300000,
43
+ async run({ assert, config, Manager, assistant, skip }) {
44
+ const env = process.env;
45
+ const blogConfig = Manager.config.blog;
46
+
47
+ // --- Default mode: config validation ---
48
+ if (!env.TEST_EXTENDED_MODE) {
49
+ assert.ok(blogConfig, 'blog config exists');
50
+ assert.ok(blogConfig.platform, 'blog.platform is set');
51
+ assert.equal(typeof blogConfig.enabled, 'boolean', 'blog.enabled is a boolean');
52
+
53
+ const contentArray = powertools.arrayify(blogConfig.content);
54
+ assert.ok(contentArray.length > 0, 'blog.content has at least one entry');
55
+
56
+ const entry = contentArray[0];
57
+ assert.ok(Array.isArray(entry.sources), 'first entry has sources array');
58
+ assert.ok(entry.sources.length > 0, 'first entry has at least one source');
59
+
60
+ // Verify source types are valid
61
+ for (const source of entry.sources) {
62
+ const validTypes = source === '$brand'
63
+ || source === '$parent'
64
+ || source.startsWith('$feed:')
65
+ || source.startsWith('http')
66
+ || typeof source === 'string';
67
+ assert.ok(validTypes, `source "${source}" is a valid type`);
68
+ }
69
+
70
+ // Verify provider exists
71
+ const providerPath = path.join(__dirname, '..', '..', 'src', 'manager', 'libraries', 'content', `${blogConfig.platform}.js`);
72
+ assert.ok(jetpack.exists(providerPath), `provider "${blogConfig.platform}" exists at ${providerPath}`);
73
+
74
+ console.log(`\n[blog-generate] Config OK:`);
75
+ console.log(` platform: ${blogConfig.platform}`);
76
+ console.log(` enabled: ${blogConfig.enabled}`);
77
+ console.log(` entries: ${contentArray.length}`);
78
+ console.log(` sources: ${entry.sources.join(', ')}`);
79
+ console.log(` categories: ${(entry.categories || []).join(', ') || '(none)'}`);
80
+ console.log(` tone: ${entry.tone || '(default)'}`);
81
+ console.log(` (Set TEST_EXTENDED_MODE=1 to run the full AI pipeline.)`);
82
+ return;
83
+ }
84
+
85
+ // --- Extended mode: full AI pipeline ---
86
+ if (!blogConfig?.enabled) {
87
+ return skip('blog.enabled is false in config');
88
+ }
89
+
90
+ const publisherPath = path.join(__dirname, '..', '..', 'src', 'manager', 'events', 'cron', 'daily', 'blog-auto-publisher.js');
91
+ const publisher = require(publisherPath);
92
+
93
+ // Get content entries
94
+ const contentArray = powertools.arrayify(blogConfig.content);
95
+ assert.ok(contentArray.length > 0, 'blog.content has at least one entry');
96
+
97
+ // Clone and override for test
98
+ const content = JSON.parse(JSON.stringify(contentArray));
99
+ content[0].chance = 1.0;
100
+ content[0].quantity = 1;
101
+
102
+ // Override source if env specified
103
+ if (env.BLOG_SOURCE) {
104
+ content[0].sources = [env.BLOG_SOURCE];
105
+ }
106
+
107
+ Manager.config.blog.content = content;
108
+
109
+ // Output dir
110
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').replace(/-(\d{3})Z$/, '');
111
+ const outDir = path.join(process.cwd(), '..', '.temp', 'blog', `run-${stamp}`);
112
+ jetpack.dir(outDir);
113
+
114
+ console.log(`\n[blog-generate] Extended mode — full AI pipeline`);
115
+ console.log(` platform: ${blogConfig.platform}`);
116
+ console.log(` sources: ${content[0].sources.join(', ')}`);
117
+ console.log(` publish: ${env.BLOG_NO_PUBLISH ? 'NO (BLOG_NO_PUBLISH=1)' : 'YES'}`);
118
+ console.log(` output: ${outDir}`);
119
+
120
+ // Intercept publishArticle if BLOG_NO_PUBLISH is set
121
+ if (env.BLOG_NO_PUBLISH) {
122
+ const provider = require(path.join(__dirname, '..', '..', 'src', 'manager', 'libraries', 'content', `${blogConfig.platform}.js`));
123
+ const originalPublish = provider.publishArticle;
124
+ provider.publishArticle = async (ast, args) => {
125
+ console.log(`[blog-generate] SKIPPED publishArticle (BLOG_NO_PUBLISH=1)`);
126
+ console.log(` title: ${args.article?.title || '(unknown)'}`);
127
+ return { post: null, url: null, slug: 'dry-run', path: null };
128
+ };
129
+
130
+ // Restore after test
131
+ process.on('exit', () => { provider.publishArticle = originalPublish; });
132
+ }
133
+
134
+ // Run the publisher
135
+ await publisher({
136
+ Manager,
137
+ assistant,
138
+ context: {},
139
+ libraries: Manager.libraries,
140
+ });
141
+
142
+ // Write metadata
143
+ jetpack.write(path.join(outDir, 'metadata.json'), JSON.stringify({
144
+ mode: 'extended',
145
+ platform: blogConfig.platform,
146
+ sources: content[0].sources,
147
+ published: !env.BLOG_NO_PUBLISH,
148
+ timestamp: new Date().toISOString(),
149
+ }, null, 2));
150
+
151
+ console.log(`\n[blog-generate] Done. Output: ${outDir}`);
152
+
153
+ // Auto-open (macOS)
154
+ if (env.BLOG_OPEN === '1' && process.platform === 'darwin') {
155
+ try { execSync(`open "${outDir}"`); } catch (e) { /* no-op */ }
156
+ }
157
+
158
+ assert.ok(true, 'Blog auto-publisher completed');
159
+ },
160
+ };