@julioventura/opensquad 0.1.17
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/README.md +433 -0
- package/_opensquad/config/playwright.config.json +11 -0
- package/_opensquad/core/architect.agent.yaml +112 -0
- package/_opensquad/core/best-practices/_catalog.yaml +126 -0
- package/_opensquad/core/best-practices/blog-post.md +132 -0
- package/_opensquad/core/best-practices/blog-seo.md +127 -0
- package/_opensquad/core/best-practices/brand-resolution-checklist.md +172 -0
- package/_opensquad/core/best-practices/copywriting.md +441 -0
- package/_opensquad/core/best-practices/data-analysis.md +401 -0
- package/_opensquad/core/best-practices/email-newsletter.md +118 -0
- package/_opensquad/core/best-practices/email-sales.md +110 -0
- package/_opensquad/core/best-practices/image-design.md +348 -0
- package/_opensquad/core/best-practices/instagram-feed.md +235 -0
- package/_opensquad/core/best-practices/instagram-reels.md +112 -0
- package/_opensquad/core/best-practices/instagram-stories.md +107 -0
- package/_opensquad/core/best-practices/linkedin-article.md +116 -0
- package/_opensquad/core/best-practices/linkedin-post.md +121 -0
- package/_opensquad/core/best-practices/researching.md +349 -0
- package/_opensquad/core/best-practices/review.md +269 -0
- package/_opensquad/core/best-practices/run-recovery.md +61 -0
- package/_opensquad/core/best-practices/social-networks-publishing.md +327 -0
- package/_opensquad/core/best-practices/squad-creation-checklist.md +32 -0
- package/_opensquad/core/best-practices/strategist.md +344 -0
- package/_opensquad/core/best-practices/technical-writing.md +365 -0
- package/_opensquad/core/best-practices/twitter-post.md +105 -0
- package/_opensquad/core/best-practices/twitter-thread.md +122 -0
- package/_opensquad/core/best-practices/whatsapp-broadcast.md +107 -0
- package/_opensquad/core/best-practices/youtube-script.md +122 -0
- package/_opensquad/core/best-practices/youtube-shorts.md +112 -0
- package/_opensquad/core/defaults/youtube-video-assembly.json +84 -0
- package/_opensquad/core/prompts/build.prompt.md +613 -0
- package/_opensquad/core/prompts/design.prompt.md +606 -0
- package/_opensquad/core/prompts/discovery.prompt.md +377 -0
- package/_opensquad/core/prompts/sherlock-instagram.md +123 -0
- package/_opensquad/core/prompts/sherlock-linkedin.md +73 -0
- package/_opensquad/core/prompts/sherlock-shared.md +684 -0
- package/_opensquad/core/prompts/sherlock-twitter.md +78 -0
- package/_opensquad/core/prompts/sherlock-youtube.md +85 -0
- package/_opensquad/core/runner.pipeline.md +743 -0
- package/_opensquad/core/skills.engine.md +384 -0
- package/bin/opensquad.js +108 -0
- package/dashboard/index.html +15 -0
- package/dashboard/package-lock.json +1964 -0
- package/dashboard/package.json +28 -0
- package/dashboard/public/assets/avatars/Female1_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Female1_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Female1_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female1_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female2_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Female2_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Female2_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female2_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female3_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female3_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female3_wave.png +0 -0
- package/dashboard/public/assets/avatars/Female4_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female4_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female4_wave.png +0 -0
- package/dashboard/public/assets/avatars/Female5_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female5_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female5_wave.png +0 -0
- package/dashboard/public/assets/avatars/Female6_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female6_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female6_wave.png +0 -0
- package/dashboard/public/assets/avatars/Male1_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Male1_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Male1_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male1_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male2_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Male2_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Male2_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male2_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male3_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male3_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male3_wave.png +0 -0
- package/dashboard/public/assets/avatars/Male4_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male4_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male4_wave.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_down.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_down_coding-1.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_down_coding.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_up.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_down.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_down_coding-1.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_down_coding.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_up.png +0 -0
- package/dashboard/public/assets/furniture/armchair_tan.png +0 -0
- package/dashboard/public/assets/furniture/armchair_tan_down.png +0 -0
- package/dashboard/public/assets/furniture/backpack_blue.png +0 -0
- package/dashboard/public/assets/furniture/backpack_red.png +0 -0
- package/dashboard/public/assets/furniture/blinds.png +0 -0
- package/dashboard/public/assets/furniture/blinds_large_closed_white.png +0 -0
- package/dashboard/public/assets/furniture/bookshelf.png +0 -0
- package/dashboard/public/assets/furniture/bookshelf_purple_tall.png +0 -0
- package/dashboard/public/assets/furniture/bulletin_board.png +0 -0
- package/dashboard/public/assets/furniture/clock.png +0 -0
- package/dashboard/public/assets/furniture/coffee_mug.png +0 -0
- package/dashboard/public/assets/furniture/coffee_mug_blue.png +0 -0
- package/dashboard/public/assets/furniture/coffee_table.png +0 -0
- package/dashboard/public/assets/furniture/coffeepot_right.png +0 -0
- package/dashboard/public/assets/furniture/coffeetable_black_horizontal.png +0 -0
- package/dashboard/public/assets/furniture/couch.png +0 -0
- package/dashboard/public/assets/furniture/couch_tan_down.png +0 -0
- package/dashboard/public/assets/furniture/cushion_blue.png +0 -0
- package/dashboard/public/assets/furniture/cushion_tan.png +0 -0
- package/dashboard/public/assets/furniture/desk_wood.png +0 -0
- package/dashboard/public/assets/furniture/fancy_rug.png +0 -0
- package/dashboard/public/assets/furniture/fancy_rug_wide.png +0 -0
- package/dashboard/public/assets/furniture/flowers1.png +0 -0
- package/dashboard/public/assets/furniture/flowers2.png +0 -0
- package/dashboard/public/assets/furniture/lamp_tan.png +0 -0
- package/dashboard/public/assets/furniture/lantern.png +0 -0
- package/dashboard/public/assets/furniture/monstera.png +0 -0
- package/dashboard/public/assets/furniture/monstera_small.png +0 -0
- package/dashboard/public/assets/furniture/picture_frame.png +0 -0
- package/dashboard/public/assets/furniture/plant1.png +0 -0
- package/dashboard/public/assets/furniture/plant2.png +0 -0
- package/dashboard/public/assets/furniture/plant3.png +0 -0
- package/dashboard/public/assets/furniture/plant_poof.png +0 -0
- package/dashboard/public/assets/furniture/plant_spindly.png +0 -0
- package/dashboard/public/assets/furniture/poster_blue.png +0 -0
- package/dashboard/public/assets/furniture/rug.png +0 -0
- package/dashboard/public/assets/furniture/succulent_blue.png +0 -0
- package/dashboard/public/assets/furniture/succulent_green.png +0 -0
- package/dashboard/public/assets/furniture/treasurechest_closed_gold.png +0 -0
- package/dashboard/public/assets/furniture/water_cooler_better.png +0 -0
- package/dashboard/public/assets/furniture/whiteboard.png +0 -0
- package/dashboard/public/assets/furniture/whiteboard_stand_graph.png +0 -0
- package/dashboard/public/assets/furniture/window_blinds_open.png +0 -0
- package/dashboard/src/App.tsx +46 -0
- package/dashboard/src/components/RunDashboardButton.tsx +92 -0
- package/dashboard/src/components/SquadCard.tsx +49 -0
- package/dashboard/src/components/SquadSelector.tsx +67 -0
- package/dashboard/src/components/StatusBadge.tsx +32 -0
- package/dashboard/src/components/StatusBar.tsx +116 -0
- package/dashboard/src/hooks/useSquadSocket.ts +135 -0
- package/dashboard/src/lib/formatTime.ts +16 -0
- package/dashboard/src/lib/normalizeState.ts +25 -0
- package/dashboard/src/main.tsx +10 -0
- package/dashboard/src/office/AgentSprite.ts +241 -0
- package/dashboard/src/office/OfficeScene.ts +153 -0
- package/dashboard/src/office/PhaserGame.tsx +80 -0
- package/dashboard/src/office/RoomBuilder.ts +190 -0
- package/dashboard/src/office/assetKeys.ts +150 -0
- package/dashboard/src/office/palette.ts +32 -0
- package/dashboard/src/plugin/squadWatcher.ts +397 -0
- package/dashboard/src/store/useSquadStore.ts +56 -0
- package/dashboard/src/styles/globals.css +36 -0
- package/dashboard/src/types/state.ts +63 -0
- package/dashboard/src/vite-env.d.ts +1 -0
- package/dashboard/tsconfig.json +24 -0
- package/dashboard/vite.config.ts +13 -0
- package/package.json +59 -0
- package/public/sfx/slide-transition-sfx.mp3 +0 -0
- package/skills/README.md +84 -0
- package/skills/apify/SKILL.md +55 -0
- package/skills/blotato/SKILL.md +63 -0
- package/skills/canva/SKILL.md +60 -0
- package/skills/higgsfield/SKILL.md +147 -0
- package/skills/image-ai-generator/SKILL.md +124 -0
- package/skills/image-ai-generator/scripts/generate.py +175 -0
- package/skills/image-creator/SKILL.md +166 -0
- package/skills/image-creator/editorial-slide-template.js +645 -0
- package/skills/image-fetcher/SKILL.md +91 -0
- package/skills/imgbb-uploader/SKILL.md +73 -0
- package/skills/imgbb-uploader/scripts/upload.js +125 -0
- package/skills/instagram-publisher/README.md +36 -0
- package/skills/instagram-publisher/SKILL.md +231 -0
- package/skills/instagram-publisher/scripts/publish-playwright.js +418 -0
- package/skills/instagram-publisher/scripts/publish.js +521 -0
- package/skills/opensquad-agent-creator/SKILL.md +192 -0
- package/skills/opensquad-skill-creator/SKILL.md +420 -0
- package/skills/opensquad-skill-creator/agents/analyzer.md +274 -0
- package/skills/opensquad-skill-creator/agents/comparator.md +202 -0
- package/skills/opensquad-skill-creator/agents/grader.md +223 -0
- package/skills/opensquad-skill-creator/assets/eval_review.html +146 -0
- package/skills/opensquad-skill-creator/eval-viewer/generate_review.py +471 -0
- package/skills/opensquad-skill-creator/eval-viewer/viewer.html +1325 -0
- package/skills/opensquad-skill-creator/references/schemas.md +430 -0
- package/skills/opensquad-skill-creator/references/skill-format.md +235 -0
- package/skills/opensquad-skill-creator/scripts/__init__.py +0 -0
- package/skills/opensquad-skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/skills/opensquad-skill-creator/scripts/quick_validate.py +103 -0
- package/skills/opensquad-skill-creator/scripts/run_eval.py +310 -0
- package/skills/opensquad-skill-creator/scripts/utils.py +47 -0
- package/skills/pdf-extractor/SKILL.md +57 -0
- package/skills/pdf-extractor/scripts/extract.py +82 -0
- package/skills/resend/SKILL.md +80 -0
- package/skills/run-dashboard/README.md +93 -0
- package/skills/run-dashboard/SKILL.md +173 -0
- package/skills/run-dashboard/scripts/finalize-state.js +273 -0
- package/skills/run-dashboard/scripts/generate.js +1296 -0
- package/skills/run-dashboard/scripts/serve.js +135 -0
- package/skills/run-dashboard/templates/run-dashboard-simple.template.html +191 -0
- package/skills/run-dashboard/templates/run-dashboard.template.html +1164 -0
- package/skills/smtp-sender/SKILL.md +88 -0
- package/skills/smtp-sender/scripts/send.js +478 -0
- package/skills/template-designer/SKILL.md +201 -0
- package/skills/template-designer/base-templates/model-a.html +27 -0
- package/skills/template-designer/base-templates/model-b.html +31 -0
- package/skills/template-designer/base-templates/model-c.html +42 -0
- package/skills/youtube-publisher/SKILL.md +232 -0
- package/skills/youtube-publisher/scripts/publish.js +2078 -0
- package/src/agents-cli.js +158 -0
- package/src/agents.js +134 -0
- package/src/i18n.js +48 -0
- package/src/init.js +442 -0
- package/src/locales/en.json +79 -0
- package/src/locales/es.json +78 -0
- package/src/locales/pt-BR.json +78 -0
- package/src/logger.js +38 -0
- package/src/prompt.js +46 -0
- package/src/readme/README.md +146 -0
- package/src/runs.js +318 -0
- package/src/skills-cli.js +157 -0
- package/src/skills.js +146 -0
- package/src/supabase-cli.js +584 -0
- package/src/update.js +169 -0
- package/templates/_opensquad/.opensquad-version +1 -0
- package/templates/_opensquad/_investigations/.gitkeep +0 -0
- package/templates/ide-templates/antigravity/.agent/rules/opensquad.md +68 -0
- package/templates/ide-templates/antigravity/.agent/workflows/opensquad.md +102 -0
- package/templates/ide-templates/claude-code/.claude/skills/opensquad/SKILL.md +182 -0
- package/templates/ide-templates/claude-code/.mcp.json +8 -0
- package/templates/ide-templates/claude-code/CLAUDE.md +57 -0
- package/templates/ide-templates/codex/.agents/skills/opensquad/SKILL.md +6 -0
- package/templates/ide-templates/codex/AGENTS.md +120 -0
- package/templates/ide-templates/cursor/.cursor/commands/opensquad.md +9 -0
- package/templates/ide-templates/cursor/.cursor/mcp.json +8 -0
- package/templates/ide-templates/cursor/.cursor/rules/opensquad.mdc +62 -0
- package/templates/ide-templates/cursor/.cursorignore +3 -0
- package/templates/ide-templates/gemini-cli/.gemini/settings.json +8 -0
- package/templates/ide-templates/gemini-cli/.gemini/skills/opensquad/SKILL.md +186 -0
- package/templates/ide-templates/gemini-cli/GEMINI.md +57 -0
- package/templates/ide-templates/opencode/.opencode/commands/opensquad.md +9 -0
- package/templates/ide-templates/opencode/AGENTS.md +120 -0
- package/templates/ide-templates/qwen-code/.qwen/settings.json +8 -0
- package/templates/ide-templates/qwen-code/.qwen/skills/opensquad/SKILL.md +182 -0
- package/templates/ide-templates/qwen-code/QWEN.md +57 -0
- package/templates/ide-templates/trae/.trae/mcp.json +8 -0
- package/templates/ide-templates/trae/.trae/rules/opensquad.md +64 -0
- package/templates/ide-templates/vscode-copilot/.github/copilot-instructions.md +59 -0
- package/templates/ide-templates/vscode-copilot/.github/prompts/opensquad.prompt.md +209 -0
- package/templates/ide-templates/vscode-copilot/.vscode/mcp.json +8 -0
- package/templates/ide-templates/vscode-copilot/.vscode/settings.json +3 -0
- package/templates/package.json +8 -0
- package/templates/squads/.gitkeep +0 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Instagram Carousel Publisher (with optional Facebook cross-post)
|
|
3
|
+
// Usage: node --env-file=.env publish.js --images "slide1.jpg,slide2.jpg" --caption "..." [--dry-run] [--facebook]
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { extname, resolve } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
// āā Argument parsing āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
10
|
+
|
|
11
|
+
export function parseArgs(argv) {
|
|
12
|
+
const args = {
|
|
13
|
+
images: [],
|
|
14
|
+
caption: '',
|
|
15
|
+
dryRun: false,
|
|
16
|
+
facebook: false,
|
|
17
|
+
facebookOnly: false,
|
|
18
|
+
instagramAccessTokenEnv: null,
|
|
19
|
+
instagramUserIdEnv: null,
|
|
20
|
+
facebookPageIdEnv: null,
|
|
21
|
+
facebookPageAccessTokenEnv: null,
|
|
22
|
+
};
|
|
23
|
+
for (let i = 2; i < argv.length; i++) {
|
|
24
|
+
if (argv[i] === '--images') {
|
|
25
|
+
if (i + 1 < argv.length) args.images = argv[++i].split(',').map(s => s.trim());
|
|
26
|
+
}
|
|
27
|
+
else if (argv[i] === '--caption') {
|
|
28
|
+
if (i + 1 < argv.length) args.caption = argv[++i];
|
|
29
|
+
}
|
|
30
|
+
else if (argv[i] === '--dry-run') args.dryRun = true;
|
|
31
|
+
else if (argv[i] === '--facebook') args.facebook = true;
|
|
32
|
+
else if (argv[i] === '--facebook-only') {
|
|
33
|
+
args.facebook = true;
|
|
34
|
+
args.facebookOnly = true;
|
|
35
|
+
}
|
|
36
|
+
else if (argv[i] === '--instagram-access-token-env') {
|
|
37
|
+
if (i + 1 < argv.length) args.instagramAccessTokenEnv = argv[++i];
|
|
38
|
+
}
|
|
39
|
+
else if (argv[i] === '--instagram-user-id-env') {
|
|
40
|
+
if (i + 1 < argv.length) args.instagramUserIdEnv = argv[++i];
|
|
41
|
+
}
|
|
42
|
+
else if (argv[i] === '--facebook-page-id-env') {
|
|
43
|
+
if (i + 1 < argv.length) args.facebookPageIdEnv = argv[++i];
|
|
44
|
+
}
|
|
45
|
+
else if (argv[i] === '--facebook-page-access-token-env') {
|
|
46
|
+
if (i + 1 < argv.length) args.facebookPageAccessTokenEnv = argv[++i];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return args;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const DEFAULT_ENV_KEYS = {
|
|
53
|
+
instagramAccessToken: 'INSTAGRAM_ACCESS_TOKEN',
|
|
54
|
+
instagramUserId: 'INSTAGRAM_USER_ID',
|
|
55
|
+
facebookPageId: 'FACEBOOK_PAGE_ID',
|
|
56
|
+
facebookPageAccessToken: 'FACEBOOK_PAGE_ACCESS_TOKEN',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export function resolveConfiguredEnv(env, options = {}) {
|
|
60
|
+
const instagramAccessTokenKey = options.instagramAccessTokenEnv || DEFAULT_ENV_KEYS.instagramAccessToken;
|
|
61
|
+
const instagramUserIdKey = options.instagramUserIdEnv || DEFAULT_ENV_KEYS.instagramUserId;
|
|
62
|
+
const facebookPageIdKey = options.facebookPageIdEnv || DEFAULT_ENV_KEYS.facebookPageId;
|
|
63
|
+
const facebookPageAccessTokenKey = options.facebookPageAccessTokenEnv || DEFAULT_ENV_KEYS.facebookPageAccessToken;
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
instagramAccessTokenKey,
|
|
67
|
+
instagramUserIdKey,
|
|
68
|
+
facebookPageIdKey,
|
|
69
|
+
facebookPageAccessTokenKey,
|
|
70
|
+
instagramAccessToken: env[instagramAccessTokenKey],
|
|
71
|
+
instagramUserId: env[instagramUserIdKey],
|
|
72
|
+
facebookPageId: env[facebookPageIdKey],
|
|
73
|
+
facebookPageAccessToken: env[facebookPageAccessTokenKey],
|
|
74
|
+
imgbbApiKey: env.IMGBB_API_KEY,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// āā Image upload (imgBB) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
79
|
+
|
|
80
|
+
export async function uploadToImgBB(imagePath, apiKey) {
|
|
81
|
+
const absolutePath = resolve(imagePath);
|
|
82
|
+
const fileBuffer = readFileSync(absolutePath);
|
|
83
|
+
const base64Image = fileBuffer.toString('base64');
|
|
84
|
+
const form = new FormData();
|
|
85
|
+
form.append('key', apiKey);
|
|
86
|
+
form.append('image', base64Image);
|
|
87
|
+
const res = await fetch('https://api.imgbb.com/1/upload', {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
body: form,
|
|
90
|
+
});
|
|
91
|
+
if (!res.ok) throw new Error(`imgBB upload failed [${res.status}]: ${await res.text()}`);
|
|
92
|
+
const json = await res.json();
|
|
93
|
+
if (!json.success) throw new Error(`imgBB upload failed: ${JSON.stringify(json)}`);
|
|
94
|
+
return json.data.url;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// āā Facebook Pages API āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
98
|
+
|
|
99
|
+
const FB_BASE = 'https://graph.facebook.com/v21.0';
|
|
100
|
+
|
|
101
|
+
function detectImageMimeType(imagePath) {
|
|
102
|
+
const extension = extname(imagePath).toLowerCase();
|
|
103
|
+
if (extension === '.png') return 'image/png';
|
|
104
|
+
return 'image/jpeg';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function shouldRetryFacebookPhotoUploadLocally(errorText) {
|
|
108
|
+
const normalized = errorText.toLowerCase();
|
|
109
|
+
return normalized.includes('missing or invalid image file')
|
|
110
|
+
|| normalized.includes('imagem obrigat')
|
|
111
|
+
|| normalized.includes('invalid image file')
|
|
112
|
+
|| normalized.includes('error_subcode":2069019');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function uploadPhotoToPage(pageId, imageUrl, pageToken, deps = {}) {
|
|
116
|
+
const {
|
|
117
|
+
fetchImpl = fetch,
|
|
118
|
+
localImagePath = null,
|
|
119
|
+
} = deps;
|
|
120
|
+
const params = new URLSearchParams({
|
|
121
|
+
url: imageUrl,
|
|
122
|
+
published: 'false',
|
|
123
|
+
access_token: pageToken,
|
|
124
|
+
});
|
|
125
|
+
const res = await fetchImpl(`${FB_BASE}/${pageId}/photos?${params}`, { method: 'POST' });
|
|
126
|
+
if (!res.ok) {
|
|
127
|
+
const errorText = await res.text();
|
|
128
|
+
|
|
129
|
+
if (!localImagePath || !shouldRetryFacebookPhotoUploadLocally(errorText)) {
|
|
130
|
+
throw new Error(`uploadPhotoToPage failed [${res.status}]: ${errorText}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const absolutePath = resolve(localImagePath);
|
|
134
|
+
const fileBuffer = readFileSync(absolutePath);
|
|
135
|
+
const form = new FormData();
|
|
136
|
+
form.append('published', 'false');
|
|
137
|
+
form.append('access_token', pageToken);
|
|
138
|
+
form.append('source', new Blob([fileBuffer], { type: detectImageMimeType(absolutePath) }), absolutePath.split(/[/\\]/).pop() || 'upload.jpg');
|
|
139
|
+
|
|
140
|
+
const fallbackRes = await fetchImpl(`${FB_BASE}/${pageId}/photos`, {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
body: form,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (!fallbackRes.ok) {
|
|
146
|
+
throw new Error(`uploadPhotoToPage failed [${fallbackRes.status}]: ${await fallbackRes.text()}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return (await fallbackRes.json()).id;
|
|
150
|
+
}
|
|
151
|
+
return (await res.json()).id;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function createFacebookPost(pageId, photoIds, caption, pageToken, deps = {}) {
|
|
155
|
+
const { fetchImpl = fetch } = deps;
|
|
156
|
+
const params = new URLSearchParams({
|
|
157
|
+
message: caption,
|
|
158
|
+
access_token: pageToken,
|
|
159
|
+
attached_media: JSON.stringify(photoIds.map(id => ({ media_fbid: id }))),
|
|
160
|
+
});
|
|
161
|
+
const res = await fetchImpl(`${FB_BASE}/${pageId}/feed`, {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: {
|
|
164
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
165
|
+
},
|
|
166
|
+
body: params,
|
|
167
|
+
});
|
|
168
|
+
if (!res.ok) throw new Error(`createFacebookPost failed [${res.status}]: ${await res.text()}`);
|
|
169
|
+
return (await res.json()).id;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function getFacebookIdentity(accessToken) {
|
|
173
|
+
const params = new URLSearchParams({ fields: 'id,name', access_token: accessToken });
|
|
174
|
+
const res = await fetch(`${FB_BASE}/me?${params}`);
|
|
175
|
+
if (!res.ok) throw new Error(`getFacebookIdentity failed [${res.status}]: ${await res.text()}`);
|
|
176
|
+
return res.json();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function listFacebookAccounts(accessToken) {
|
|
180
|
+
const params = new URLSearchParams({
|
|
181
|
+
fields: 'id,name,access_token',
|
|
182
|
+
limit: '200',
|
|
183
|
+
access_token: accessToken,
|
|
184
|
+
});
|
|
185
|
+
const res = await fetch(`${FB_BASE}/me/accounts?${params}`);
|
|
186
|
+
if (!res.ok) throw new Error(`listFacebookAccounts failed [${res.status}]: ${await res.text()}`);
|
|
187
|
+
const json = await res.json();
|
|
188
|
+
return json.data ?? [];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function resolveFacebookPageAccessToken(options, deps = {}) {
|
|
192
|
+
const {
|
|
193
|
+
pageId,
|
|
194
|
+
configuredToken,
|
|
195
|
+
fallbackUserToken,
|
|
196
|
+
} = options;
|
|
197
|
+
const {
|
|
198
|
+
getFacebookIdentityImpl = getFacebookIdentity,
|
|
199
|
+
listFacebookAccountsImpl = listFacebookAccounts,
|
|
200
|
+
} = deps;
|
|
201
|
+
|
|
202
|
+
if (!pageId) return null;
|
|
203
|
+
|
|
204
|
+
const tryResolveFromToken = async (token, sourceBase) => {
|
|
205
|
+
if (!token) return null;
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const identity = await getFacebookIdentityImpl(token);
|
|
209
|
+
if (identity?.id === pageId) {
|
|
210
|
+
return { token, source: `${sourceBase}-page-token` };
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
// Some tokens are not valid for /me identity introspection. Continue to /me/accounts.
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const accounts = await listFacebookAccountsImpl(token);
|
|
218
|
+
const matchingPage = accounts.find(account => account.id === pageId && account.access_token);
|
|
219
|
+
if (matchingPage) {
|
|
220
|
+
return { token: matchingPage.access_token, source: `${sourceBase}-accounts` };
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return null;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const configuredResolution = await tryResolveFromToken(configuredToken, 'configured');
|
|
230
|
+
if (configuredResolution) return configuredResolution;
|
|
231
|
+
|
|
232
|
+
if (fallbackUserToken && fallbackUserToken !== configuredToken) {
|
|
233
|
+
const fallbackResolution = await tryResolveFromToken(fallbackUserToken, 'instagram');
|
|
234
|
+
if (fallbackResolution) return fallbackResolution;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// āā Instagram Graph API āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
241
|
+
|
|
242
|
+
const IG_BASE = 'https://graph.facebook.com/v21.0';
|
|
243
|
+
|
|
244
|
+
export async function createChildContainer(userId, imageUrl, accessToken) {
|
|
245
|
+
const params = new URLSearchParams({
|
|
246
|
+
image_url: imageUrl,
|
|
247
|
+
is_carousel_item: 'true',
|
|
248
|
+
access_token: accessToken,
|
|
249
|
+
});
|
|
250
|
+
const res = await fetch(`${IG_BASE}/${userId}/media?${params}`, { method: 'POST' });
|
|
251
|
+
if (!res.ok) throw new Error(`createChildContainer failed [${res.status}]: ${await res.text()}`);
|
|
252
|
+
return (await res.json()).id;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function getContainerStatus(containerId, accessToken) {
|
|
256
|
+
const params = new URLSearchParams({ fields: 'status_code', access_token: accessToken });
|
|
257
|
+
const res = await fetch(`${IG_BASE}/${containerId}?${params}`);
|
|
258
|
+
if (!res.ok) throw new Error(`getContainerStatus failed [${res.status}]: ${await res.text()}`);
|
|
259
|
+
return (await res.json()).status_code;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function pollUntilFinished(containerId, accessToken, timeoutMs = 60_000, intervalMs = 3_000) {
|
|
263
|
+
const deadline = Date.now() + timeoutMs;
|
|
264
|
+
while (Date.now() < deadline) {
|
|
265
|
+
const status = await getContainerStatus(containerId, accessToken);
|
|
266
|
+
if (status === 'FINISHED') return;
|
|
267
|
+
if (status === 'ERROR') throw new Error(`Container ${containerId} entered ERROR state`);
|
|
268
|
+
await new Promise(r => setTimeout(r, intervalMs));
|
|
269
|
+
}
|
|
270
|
+
throw new Error(`Container ${containerId} timed out after ${timeoutMs}ms`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function createCarouselContainer(userId, childIds, caption, accessToken) {
|
|
274
|
+
const params = new URLSearchParams({
|
|
275
|
+
media_type: 'CAROUSEL',
|
|
276
|
+
children: childIds.join(','),
|
|
277
|
+
caption,
|
|
278
|
+
access_token: accessToken,
|
|
279
|
+
});
|
|
280
|
+
const res = await fetch(`${IG_BASE}/${userId}/media?${params}`, { method: 'POST' });
|
|
281
|
+
if (!res.ok) throw new Error(`createCarouselContainer failed [${res.status}]: ${await res.text()}`);
|
|
282
|
+
return (await res.json()).id;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function publishMedia(userId, containerId, accessToken) {
|
|
286
|
+
const params = new URLSearchParams({ creation_id: containerId, access_token: accessToken });
|
|
287
|
+
const res = await fetch(`${IG_BASE}/${userId}/media_publish?${params}`, { method: 'POST' });
|
|
288
|
+
if (!res.ok) throw new Error(`publishMedia failed [${res.status}]: ${await res.text()}`);
|
|
289
|
+
return (await res.json()).id;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function enrichInstagramPublishErrorMessage(message) {
|
|
293
|
+
const text = String(message || '');
|
|
294
|
+
const normalized = text.toLowerCase();
|
|
295
|
+
const isKnownLivePublishBlock = normalized.includes('publishmedia failed')
|
|
296
|
+
&& (
|
|
297
|
+
normalized.includes('error_subcode":2207051')
|
|
298
|
+
|| normalized.includes('"code":4')
|
|
299
|
+
|| normalized.includes('application request limit reached')
|
|
300
|
+
|| normalized.includes('application-level rate limit')
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
if (!isKnownLivePublishBlock) {
|
|
304
|
+
return text;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return `${text}\nMeta blocked the live Graph API publish step for this app/account. If the media containers were created successfully, treat this as a platform restriction rather than an asset-order failure. Use Meta Business Suite with the approved images and caption, confirm the carousel order before the final click, then record the final Instagram/Facebook URLs and post IDs in the run artifacts.`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export async function getPermalink(mediaId, accessToken) {
|
|
311
|
+
const params = new URLSearchParams({ fields: 'permalink', access_token: accessToken });
|
|
312
|
+
const res = await fetch(`${IG_BASE}/${mediaId}?${params}`);
|
|
313
|
+
if (!res.ok) return null; // non-fatal ā just skip the URL display
|
|
314
|
+
const json = await res.json();
|
|
315
|
+
return json.permalink ?? null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// āā Main āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
319
|
+
|
|
320
|
+
export async function publishCarousel(options, env = process.env, deps = {}) {
|
|
321
|
+
const {
|
|
322
|
+
uploadToImgBB: uploadToImgBBImpl = uploadToImgBB,
|
|
323
|
+
createChildContainer: createChildContainerImpl = createChildContainer,
|
|
324
|
+
pollUntilFinished: pollUntilFinishedImpl = pollUntilFinished,
|
|
325
|
+
createCarouselContainer: createCarouselContainerImpl = createCarouselContainer,
|
|
326
|
+
publishMedia: publishMediaImpl = publishMedia,
|
|
327
|
+
getPermalink: getPermalinkImpl = getPermalink,
|
|
328
|
+
uploadPhotoToPage: uploadPhotoToPageImpl = uploadPhotoToPage,
|
|
329
|
+
createFacebookPost: createFacebookPostImpl = createFacebookPost,
|
|
330
|
+
resolveFacebookPageAccessToken: resolveFacebookPageAccessTokenImpl = resolveFacebookPageAccessToken,
|
|
331
|
+
logger = console,
|
|
332
|
+
} = deps;
|
|
333
|
+
|
|
334
|
+
const {
|
|
335
|
+
images,
|
|
336
|
+
caption,
|
|
337
|
+
dryRun,
|
|
338
|
+
facebook,
|
|
339
|
+
facebookOnly,
|
|
340
|
+
instagramAccessTokenEnv,
|
|
341
|
+
instagramUserIdEnv,
|
|
342
|
+
facebookPageIdEnv,
|
|
343
|
+
facebookPageAccessTokenEnv,
|
|
344
|
+
} = options;
|
|
345
|
+
|
|
346
|
+
if (!images.length) throw new Error('--images is required (e.g. --images "slide1.jpg,slide2.jpg")');
|
|
347
|
+
if (!caption) throw new Error('--caption is required');
|
|
348
|
+
if (!facebookOnly && (images.length < 2 || images.length > 10)) {
|
|
349
|
+
throw new Error(`Instagram carousels require 2ā10 images (got ${images.length})`);
|
|
350
|
+
}
|
|
351
|
+
if (caption.length > 2200) {
|
|
352
|
+
throw new Error(`Caption exceeds Instagram's 2200-character limit (got ${caption.length})`);
|
|
353
|
+
}
|
|
354
|
+
if (facebookOnly && !facebook) {
|
|
355
|
+
throw new Error('--facebook-only requires --facebook behavior to be enabled');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const configuredEnv = resolveConfiguredEnv(env, {
|
|
359
|
+
instagramAccessTokenEnv,
|
|
360
|
+
instagramUserIdEnv,
|
|
361
|
+
facebookPageIdEnv,
|
|
362
|
+
facebookPageAccessTokenEnv,
|
|
363
|
+
});
|
|
364
|
+
const {
|
|
365
|
+
instagramAccessTokenKey,
|
|
366
|
+
instagramUserIdKey,
|
|
367
|
+
facebookPageIdKey,
|
|
368
|
+
facebookPageAccessTokenKey,
|
|
369
|
+
instagramAccessToken,
|
|
370
|
+
instagramUserId,
|
|
371
|
+
facebookPageId,
|
|
372
|
+
facebookPageAccessToken,
|
|
373
|
+
imgbbApiKey,
|
|
374
|
+
} = configuredEnv;
|
|
375
|
+
|
|
376
|
+
if (!facebookOnly && !instagramAccessToken) throw new Error(`${instagramAccessTokenKey} is not set in environment`);
|
|
377
|
+
if (!facebookOnly && !instagramUserId) throw new Error(`${instagramUserIdKey} is not set in environment`);
|
|
378
|
+
if (!imgbbApiKey) throw new Error('IMGBB_API_KEY is not set in environment. Get one at https://api.imgbb.com/');
|
|
379
|
+
if (facebook && !facebookPageId) throw new Error(`${facebookPageIdKey} is not set in environment ā required for --facebook`);
|
|
380
|
+
if (facebook && !facebookPageAccessToken) {
|
|
381
|
+
throw new Error(`${facebookPageAccessTokenKey} is not set in environment ā required for --facebook`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let fbToken = null;
|
|
385
|
+
if (facebook) {
|
|
386
|
+
const resolvedFacebookToken = await resolveFacebookPageAccessTokenImpl(
|
|
387
|
+
{
|
|
388
|
+
pageId: facebookPageId,
|
|
389
|
+
configuredToken: facebookPageAccessToken,
|
|
390
|
+
fallbackUserToken: instagramAccessToken,
|
|
391
|
+
},
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
if (!resolvedFacebookToken?.token) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
`Could not resolve a Facebook Page access token for Page ${facebookPageId}. Checked ${facebookPageAccessTokenKey} and ${instagramAccessTokenKey}.`,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
fbToken = resolvedFacebookToken.token;
|
|
401
|
+
if (resolvedFacebookToken.source !== 'configured-page-token') {
|
|
402
|
+
logger.log(`ā¹ļø Facebook Page token resolved via ${resolvedFacebookToken.source} for Page ${facebookPageId}.`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const result = {
|
|
407
|
+
status: dryRun ? 'dry-run' : 'published',
|
|
408
|
+
instagramPostId: null,
|
|
409
|
+
instagramUrl: null,
|
|
410
|
+
facebookPostId: null,
|
|
411
|
+
facebookUrl: null,
|
|
412
|
+
error: null,
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
logger.log(`šø Uploading ${images.length} image(s) to imgBB...`);
|
|
416
|
+
const imageUrls = await Promise.all(images.map(p => uploadToImgBBImpl(p, imgbbApiKey)));
|
|
417
|
+
imageUrls.forEach((url, i) => logger.log(` [${i + 1}] ${url}`));
|
|
418
|
+
|
|
419
|
+
if (facebookOnly) {
|
|
420
|
+
if (dryRun) {
|
|
421
|
+
logger.log('\nā
DRY RUN complete ā skipping final Facebook publish call.');
|
|
422
|
+
logger.log(` Facebook cross-post: would post ${images.length} images to Page ${facebookPageId}`);
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
logger.log(`\nš Publishing to Facebook Page ${facebookPageId}...`);
|
|
427
|
+
const fbPhotoIds = await Promise.all(
|
|
428
|
+
imageUrls.map((url, index) => uploadPhotoToPageImpl(facebookPageId, url, fbToken, { localImagePath: images[index] }))
|
|
429
|
+
);
|
|
430
|
+
logger.log(` Photo IDs: ${fbPhotoIds.join(', ')}`);
|
|
431
|
+
const fbPostId = await createFacebookPostImpl(facebookPageId, fbPhotoIds, caption, fbToken);
|
|
432
|
+
result.facebookPostId = fbPostId;
|
|
433
|
+
result.facebookUrl = `https://www.facebook.com/${fbPostId}`;
|
|
434
|
+
logger.log(`\nā
Facebook: Published successfully!`);
|
|
435
|
+
logger.log(` Post ID: ${fbPostId}`);
|
|
436
|
+
logger.log(` URL: ${result.facebookUrl}`);
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
logger.log('\nš¦ Creating Instagram media containers...');
|
|
441
|
+
const childIds = await Promise.all(
|
|
442
|
+
imageUrls.map(url => createChildContainerImpl(instagramUserId, url, instagramAccessToken))
|
|
443
|
+
);
|
|
444
|
+
logger.log(` Container IDs: ${childIds.join(', ')}`);
|
|
445
|
+
|
|
446
|
+
logger.log('\nā³ Waiting for containers to finish processing...');
|
|
447
|
+
await Promise.all(childIds.map(id => pollUntilFinishedImpl(id, instagramAccessToken)));
|
|
448
|
+
logger.log(' All containers ready.');
|
|
449
|
+
|
|
450
|
+
logger.log('\nš Creating carousel container...');
|
|
451
|
+
const carouselId = await createCarouselContainerImpl(
|
|
452
|
+
instagramUserId, childIds, caption, instagramAccessToken
|
|
453
|
+
);
|
|
454
|
+
await pollUntilFinishedImpl(carouselId, instagramAccessToken);
|
|
455
|
+
logger.log(` Carousel container ID: ${carouselId}`);
|
|
456
|
+
|
|
457
|
+
if (dryRun) {
|
|
458
|
+
logger.log('\nā
DRY RUN complete ā skipping final publish call.');
|
|
459
|
+
logger.log(` Carousel container ready: ${carouselId}`);
|
|
460
|
+
if (facebook) logger.log(` Facebook cross-post: would post ${images.length} images to Page ${facebookPageId}`);
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
logger.log('\nš Publishing to Instagram...');
|
|
465
|
+
let postId;
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
postId = await publishMediaImpl(instagramUserId, carouselId, instagramAccessToken);
|
|
469
|
+
} catch (error) {
|
|
470
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
471
|
+
throw new Error(enrichInstagramPublishErrorMessage(message));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const permalink = await getPermalinkImpl(postId, instagramAccessToken);
|
|
475
|
+
result.instagramPostId = postId;
|
|
476
|
+
result.instagramUrl = permalink;
|
|
477
|
+
logger.log(`\nā
Instagram: Published successfully!`);
|
|
478
|
+
logger.log(` Post ID: ${postId}`);
|
|
479
|
+
if (permalink) logger.log(` URL: ${permalink}`);
|
|
480
|
+
|
|
481
|
+
if (facebook) {
|
|
482
|
+
try {
|
|
483
|
+
logger.log(`\nš Cross-posting to Facebook Page ${facebookPageId}...`);
|
|
484
|
+
const fbPhotoIds = await Promise.all(
|
|
485
|
+
imageUrls.map((url, index) => uploadPhotoToPageImpl(facebookPageId, url, fbToken, { localImagePath: images[index] }))
|
|
486
|
+
);
|
|
487
|
+
logger.log(` Photo IDs: ${fbPhotoIds.join(', ')}`);
|
|
488
|
+
const fbPostId = await createFacebookPostImpl(facebookPageId, fbPhotoIds, caption, fbToken);
|
|
489
|
+
result.facebookPostId = fbPostId;
|
|
490
|
+
result.facebookUrl = `https://www.facebook.com/${fbPostId}`;
|
|
491
|
+
logger.log(`\nā
Facebook: Published successfully!`);
|
|
492
|
+
logger.log(` Post ID: ${fbPostId}`);
|
|
493
|
+
logger.log(` URL: ${result.facebookUrl}`);
|
|
494
|
+
} catch (error) {
|
|
495
|
+
result.status = 'partial';
|
|
496
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
497
|
+
logger.error(`\nā ļø Partial result: Instagram published, but Facebook failed.`);
|
|
498
|
+
logger.error(` ${result.error}`);
|
|
499
|
+
return result;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return result;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function main() {
|
|
507
|
+
const result = await publishCarousel(parseArgs(process.argv));
|
|
508
|
+
|
|
509
|
+
if (result.status === 'partial') {
|
|
510
|
+
process.exitCode = 2;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Run only when executed directly (not when imported for tests)
|
|
515
|
+
const isMain = process.argv[1] === fileURLToPath(import.meta.url);
|
|
516
|
+
if (isMain) {
|
|
517
|
+
main().catch(err => {
|
|
518
|
+
console.error(`\nā ${err.message}`);
|
|
519
|
+
process.exit(1);
|
|
520
|
+
});
|
|
521
|
+
}
|