crewly 1.20.28 → 1.20.30
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/config/skills/agent/core/canva-create/SKILL.md +53 -0
- package/config/skills/agent/core/canva-create/execute.sh +88 -0
- package/config/skills/agent/core/canva-create/execute.test.sh +54 -0
- package/config/skills/agent/core/canva-designs/SKILL.md +51 -0
- package/config/skills/agent/core/canva-designs/execute.sh +96 -0
- package/config/skills/agent/core/canva-designs/execute.test.sh +54 -0
- package/config/skills/agent/core/canva-export/SKILL.md +52 -0
- package/config/skills/agent/core/canva-export/execute.sh +111 -0
- package/config/skills/agent/core/canva-export/execute.test.sh +55 -0
- package/config/skills/agent/core/canva-upload/SKILL.md +49 -0
- package/config/skills/agent/core/canva-upload/execute.sh +72 -0
- package/config/skills/agent/core/canva-upload/execute.test.sh +54 -0
- package/config/skills/agent/core/docs-read/SKILL.md +50 -0
- package/config/skills/agent/core/docs-read/execute.sh +68 -0
- package/config/skills/agent/core/docs-read/execute.test.sh +52 -0
- package/config/skills/agent/core/docs-write/SKILL.md +55 -0
- package/config/skills/agent/core/docs-write/execute.sh +90 -0
- package/config/skills/agent/core/docs-write/execute.test.sh +54 -0
- package/config/skills/agent/core/drive-read/SKILL.md +53 -0
- package/config/skills/agent/core/drive-read/execute.sh +79 -0
- package/config/skills/agent/core/drive-read/execute.test.sh +55 -0
- package/config/skills/agent/core/drive-search/SKILL.md +55 -0
- package/config/skills/agent/core/drive-search/execute.sh +87 -0
- package/config/skills/agent/core/drive-search/execute.test.sh +54 -0
- package/config/skills/agent/core/drive-upload/SKILL.md +54 -0
- package/config/skills/agent/core/drive-upload/execute.sh +117 -0
- package/config/skills/agent/core/drive-upload/execute.test.sh +57 -0
- package/config/skills/agent/core/sheets-read/SKILL.md +53 -0
- package/config/skills/agent/core/sheets-read/execute.sh +81 -0
- package/config/skills/agent/core/sheets-read/execute.test.sh +52 -0
- package/config/skills/agent/core/sheets-write/SKILL.md +56 -0
- package/config/skills/agent/core/sheets-write/execute.sh +103 -0
- package/config/skills/agent/core/sheets-write/execute.test.sh +57 -0
- package/config/skills/agent/core/slides-create/SKILL.md +61 -0
- package/config/skills/agent/core/slides-create/execute.sh +92 -0
- package/config/skills/agent/core/slides-create/execute.test.sh +54 -0
- package/config/skills/agent/core/slides-read/SKILL.md +49 -0
- package/config/skills/agent/core/slides-read/execute.sh +67 -0
- package/config/skills/agent/core/slides-read/execute.test.sh +50 -0
- package/dist/backend/backend/src/constants.d.ts +69 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +73 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/canva/canva.controller.d.ts +48 -0
- package/dist/backend/backend/src/controllers/canva/canva.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/canva/canva.controller.js +183 -0
- package/dist/backend/backend/src/controllers/canva/canva.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/canva/canva.routes.d.ts +22 -0
- package/dist/backend/backend/src/controllers/canva/canva.routes.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/canva/canva.routes.js +34 -0
- package/dist/backend/backend/src/controllers/canva/canva.routes.js.map +1 -0
- package/dist/backend/backend/src/controllers/google/google.controller.d.ts +100 -0
- package/dist/backend/backend/src/controllers/google/google.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/google/google.controller.js +237 -1
- package/dist/backend/backend/src/controllers/google/google.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/google/google.routes.d.ts +13 -0
- package/dist/backend/backend/src/controllers/google/google.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/google/google.routes.js +27 -1
- package/dist/backend/backend/src/controllers/google/google.routes.js.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.js +3 -0
- package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
- package/dist/backend/backend/src/services/canva/canva-token.service.d.ts +98 -0
- package/dist/backend/backend/src/services/canva/canva-token.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/canva/canva-token.service.js +198 -0
- package/dist/backend/backend/src/services/canva/canva-token.service.js.map +1 -0
- package/dist/backend/backend/src/services/canva/canva.service.d.ts +184 -0
- package/dist/backend/backend/src/services/canva/canva.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/canva/canva.service.js +274 -0
- package/dist/backend/backend/src/services/canva/canva.service.js.map +1 -0
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +2 -2
- package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
- package/dist/backend/backend/src/services/google/docs.service.d.ts +117 -0
- package/dist/backend/backend/src/services/google/docs.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/google/docs.service.js +152 -0
- package/dist/backend/backend/src/services/google/docs.service.js.map +1 -0
- package/dist/backend/backend/src/services/google/drive.service.d.ts +157 -0
- package/dist/backend/backend/src/services/google/drive.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/google/drive.service.js +203 -0
- package/dist/backend/backend/src/services/google/drive.service.js.map +1 -0
- package/dist/backend/backend/src/services/google/google-api.client.d.ts +15 -7
- package/dist/backend/backend/src/services/google/google-api.client.d.ts.map +1 -1
- package/dist/backend/backend/src/services/google/google-api.client.js +13 -6
- package/dist/backend/backend/src/services/google/google-api.client.js.map +1 -1
- package/dist/backend/backend/src/services/google/sheets.service.d.ts +118 -0
- package/dist/backend/backend/src/services/google/sheets.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/google/sheets.service.js +158 -0
- package/dist/backend/backend/src/services/google/sheets.service.js.map +1 -0
- package/dist/backend/backend/src/services/google/slides.service.d.ts +136 -0
- package/dist/backend/backend/src/services/google/slides.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/google/slides.service.js +153 -0
- package/dist/backend/backend/src/services/google/slides.service.js.map +1 -0
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +16 -3
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack.service.js +11 -7
- package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +69 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +73 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +2 -2
- package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +16 -3
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack.service.js +11 -7
- package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -1
- package/frontend/dist/assets/{index-d1bd0dec.js → index-96b51cd9.js} +243 -243
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Canva Create
|
|
3
|
+
description: Create a new Canva design in the owner's account — a preset (doc / whiteboard / presentation), a custom size (e.g. 1080x1920 for stories), or one built from an uploaded asset (via the Canva grant held by Crewly Cloud). Returns the edit link for a person to finish the design.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
category: productivity
|
|
6
|
+
skillType: claude-skill
|
|
7
|
+
assignableRoles:
|
|
8
|
+
- orchestrator
|
|
9
|
+
- team-leader
|
|
10
|
+
- developer
|
|
11
|
+
- operations
|
|
12
|
+
- ops
|
|
13
|
+
- sales
|
|
14
|
+
- support
|
|
15
|
+
- generalist
|
|
16
|
+
triggers:
|
|
17
|
+
- create canva design
|
|
18
|
+
- new design in canva
|
|
19
|
+
- make a poster in canva
|
|
20
|
+
- start a canva deck
|
|
21
|
+
tags:
|
|
22
|
+
- canva
|
|
23
|
+
- create
|
|
24
|
+
execution:
|
|
25
|
+
type: script
|
|
26
|
+
script:
|
|
27
|
+
file: execute.sh
|
|
28
|
+
interpreter: bash
|
|
29
|
+
timeoutMs: 60000
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
# Canva Create
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bash execute.sh --title "Open day poster" --size 1080x1350
|
|
36
|
+
bash execute.sh --title "Reel cover" --asset AAB… # after canva-upload
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Output
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{"success":true,"design":{"id":"DAF…","title":"Open day poster","editUrl":"https://www.canva.com/…"}}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The Connect API does not fill designs with text or generate video: create
|
|
46
|
+
the design, upload the assets, then hand the `editUrl` to a person (or
|
|
47
|
+
export an existing finished design with `canva-export`). Brand templates
|
|
48
|
+
need Canva Enterprise and are not wired.
|
|
49
|
+
|
|
50
|
+
## Failures
|
|
51
|
+
|
|
52
|
+
`{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
|
|
53
|
+
when the owner has not connected Canva (Settings → Integrations).
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# canva-create — Create a new Canva design (preset type, custom size, or from an uploaded asset)
|
|
4
|
+
#
|
|
5
|
+
# Backed by POST /api/canva/designs.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# bash execute.sh --title "Open day poster" --size 1080x1350
|
|
9
|
+
# bash execute.sh --title "Term plan" --preset presentation # doc | whiteboard | presentation
|
|
10
|
+
# bash execute.sh --title "Flyer" --asset <assetId> # start from an uploaded image
|
|
11
|
+
# bash execute.sh '{"title":"Poster","width":1080,"height":1920}'
|
|
12
|
+
# =============================================================================
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
source "${SCRIPT_DIR}/../../_common/lib.sh"
|
|
16
|
+
|
|
17
|
+
print_usage() {
|
|
18
|
+
cat <<'EOF_USAGE'
|
|
19
|
+
Usage:
|
|
20
|
+
bash execute.sh --title "Open day poster" --size 1080x1350
|
|
21
|
+
bash execute.sh --title "Term plan" --preset presentation # doc | whiteboard | presentation
|
|
22
|
+
bash execute.sh --title "Flyer" --asset <assetId> # start from an uploaded image
|
|
23
|
+
bash execute.sh '{"title":"Poster","width":1080,"height":1920}'
|
|
24
|
+
|
|
25
|
+
Options:
|
|
26
|
+
--title Design title (≤ 255 chars)
|
|
27
|
+
--preset doc | whiteboard | presentation
|
|
28
|
+
--size WIDTHxHEIGHT in px (40–8000), e.g. 1080x1920 for a vertical video/story
|
|
29
|
+
--asset Asset id from canva-upload to place on the first page
|
|
30
|
+
--help | -h Show this help
|
|
31
|
+
EOF_USAGE
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fail_from() {
|
|
35
|
+
printf '%s' "$1" | jq -c '{success: false, reason: (.details.error // .details // .error // "unknown"), hint: (.details.hint // ""), message: (.details.message // "")}' 2>/dev/null \
|
|
36
|
+
|| jq -n --arg r "$1" '{success: false, reason: $r}'
|
|
37
|
+
exit 1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
uri() { jq -rn --arg v "$1" '$v|@uri'; }
|
|
41
|
+
|
|
42
|
+
# api_call may print a one-line warning to stderr (no CREWLY_SESSION_NAME);
|
|
43
|
+
# the backend answer is always the last line. On failure print the mapped
|
|
44
|
+
# failure JSON (fail_from) and return 1.
|
|
45
|
+
call() {
|
|
46
|
+
local out
|
|
47
|
+
out=$(api_call "$@" 2>&1) || { fail_from "$(printf '%s
|
|
48
|
+
' "$out" | tail -n 1)"; }
|
|
49
|
+
printf '%s
|
|
50
|
+
' "$out" | tail -n 1
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
INPUT_JSON=""
|
|
54
|
+
if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
|
|
55
|
+
INPUT_JSON="$1"
|
|
56
|
+
shift || true
|
|
57
|
+
fi
|
|
58
|
+
TITLE=""; PRESET=""; SIZE=""; WIDTH=""; HEIGHT=""; ASSET=""
|
|
59
|
+
while [[ $# -gt 0 ]]; do
|
|
60
|
+
case "$1" in
|
|
61
|
+
--title) [ $# -ge 2 ] || error_exit "--title requires a value"; TITLE="$2"; shift 2 ;;
|
|
62
|
+
--preset) [ $# -ge 2 ] || error_exit "--preset requires a value"; PRESET="$2"; shift 2 ;;
|
|
63
|
+
--size) [ $# -ge 2 ] || error_exit "--size requires a value"; SIZE="$2"; shift 2 ;;
|
|
64
|
+
--asset) [ $# -ge 2 ] || error_exit "--asset requires a value"; ASSET="$2"; shift 2 ;;
|
|
65
|
+
--help|-h) print_usage; exit 0 ;;
|
|
66
|
+
*) error_exit "Unknown option: $1" ;;
|
|
67
|
+
esac
|
|
68
|
+
done
|
|
69
|
+
if [ -n "$INPUT_JSON" ]; then
|
|
70
|
+
INPUT=$(read_json_input "$INPUT_JSON")
|
|
71
|
+
[ -z "$TITLE" ] && TITLE=$(printf '%s' "$INPUT" | jq -r '.title // empty')
|
|
72
|
+
[ -z "$PRESET" ] && PRESET=$(printf '%s' "$INPUT" | jq -r '.preset // empty')
|
|
73
|
+
[ -z "$WIDTH" ] && WIDTH=$(printf '%s' "$INPUT" | jq -r '.width // empty')
|
|
74
|
+
[ -z "$HEIGHT" ] && HEIGHT=$(printf '%s' "$INPUT" | jq -r '.height // empty')
|
|
75
|
+
[ -z "$ASSET" ] && ASSET=$(printf '%s' "$INPUT" | jq -r '.asset // .assetId // empty')
|
|
76
|
+
fi
|
|
77
|
+
if [ -n "$SIZE" ]; then
|
|
78
|
+
[[ "$SIZE" =~ ^([0-9]+)x([0-9]+)$ ]] || error_exit "--size must look like 1080x1920"
|
|
79
|
+
WIDTH="${BASH_REMATCH[1]}"; HEIGHT="${BASH_REMATCH[2]}"
|
|
80
|
+
fi
|
|
81
|
+
[ -n "$PRESET" ] || [ -n "$WIDTH" ] || [ -n "$ASSET" ] || error_exit "give --preset, --size or --asset"
|
|
82
|
+
BODY=$(jq -cn --arg title "$TITLE" --arg preset "$PRESET" --arg w "$WIDTH" --arg h "$HEIGHT" --arg asset "$ASSET" \
|
|
83
|
+
'{} + (if $title != "" then {title: $title} else {} end)
|
|
84
|
+
+ (if $preset != "" then {preset: $preset} else {} end)
|
|
85
|
+
+ (if $w != "" then {width: ($w|tonumber), height: ($h|tonumber)} else {} end)
|
|
86
|
+
+ (if $asset != "" then {assetId: $asset} else {} end)')
|
|
87
|
+
RESPONSE=$(call POST "/canva/designs" "$BODY") || { printf '%s\n' "$RESPONSE"; exit 1; }
|
|
88
|
+
printf '%s' "$RESPONSE" | jq -c '{success: (.success // false), design: .data}'
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for canva-create — run with: bash execute.test.sh
|
|
3
|
+
# Python HTTP stub as the backend; asserts the request the skill sends and
|
|
4
|
+
# the JSON it prints. Exit 0 on pass.
|
|
5
|
+
set -u
|
|
6
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
EXEC="$HERE/execute.sh"
|
|
8
|
+
PASS=0; FAIL=0
|
|
9
|
+
check() {
|
|
10
|
+
local name="$1"; local got="$2"; local want="$3"
|
|
11
|
+
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
PORT=18821
|
|
15
|
+
STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
|
|
16
|
+
python3 -u <<'PY' >/dev/null 2>&1 &
|
|
17
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
18
|
+
import json, os
|
|
19
|
+
LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
|
|
20
|
+
class H(BaseHTTPRequestHandler):
|
|
21
|
+
def _reply(self, method, data=None):
|
|
22
|
+
open(LOG, 'w').write(json.dumps({'method': method, 'path': self.path, 'body': data}))
|
|
23
|
+
if 'no-grant' in self.path or (data or {}).get('title') == 'no-grant':
|
|
24
|
+
self.send_response(409); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
25
|
+
self.wfile.write(json.dumps({'success': False, 'error': 'not_connected', 'message': 'no grant', 'hint': 'https://cloud/start?token=j'}).encode()); return
|
|
26
|
+
self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
27
|
+
self.wfile.write(json.dumps({'success': True, 'data': {'id': 'D9', 'title': data.get('title'), 'editUrl': 'https://e'}}).encode())
|
|
28
|
+
def do_GET(self): self._reply('GET')
|
|
29
|
+
def do_POST(self):
|
|
30
|
+
n = int(self.headers.get('content-length', '0')); body = self.rfile.read(n).decode() if n else ''
|
|
31
|
+
self._reply('POST', json.loads(body or '{}'))
|
|
32
|
+
def log_message(self, *a, **k): pass
|
|
33
|
+
HTTPServer(('127.0.0.1', PORT), H).serve_forever()
|
|
34
|
+
PY
|
|
35
|
+
STUB_PID=$!
|
|
36
|
+
disown "$STUB_PID" 2>/dev/null || true
|
|
37
|
+
trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
|
|
38
|
+
for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
|
|
39
|
+
|
|
40
|
+
run() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
|
|
41
|
+
run_err() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null; }
|
|
42
|
+
|
|
43
|
+
OUT=$(run --title "Poster" --size 1080x1350)
|
|
44
|
+
check "size: output" "$OUT" '{"success":true,"design":{"id":"D9","title":"Poster","editUrl":"https://e"}}'
|
|
45
|
+
check "size: body" "$(jq -c .body "$STUB_LOG")" '{"title":"Poster","width":1080,"height":1350}'
|
|
46
|
+
run --title "Deck" --preset presentation >/dev/null
|
|
47
|
+
check "preset: body" "$(jq -c .body "$STUB_LOG")" '{"title":"Deck","preset":"presentation"}'
|
|
48
|
+
run '{"assetId":"A1"}' >/dev/null
|
|
49
|
+
check "asset: body" "$(jq -c .body "$STUB_LOG")" '{"assetId":"A1"}'
|
|
50
|
+
OUT=$(run_err --title "x" || true)
|
|
51
|
+
check "nothing to create fails" "$(printf '%s' "$OUT" | grep -c 'give --preset')" "1"
|
|
52
|
+
|
|
53
|
+
echo "canva-create: $PASS passed, $FAIL failed"
|
|
54
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Canva Designs
|
|
3
|
+
description: List or search the owner's Canva designs, or fetch one by id — returns edit/view links and thumbnails (via the Canva grant held by Crewly Cloud). Read-only.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
category: productivity
|
|
6
|
+
skillType: claude-skill
|
|
7
|
+
assignableRoles:
|
|
8
|
+
- orchestrator
|
|
9
|
+
- team-leader
|
|
10
|
+
- developer
|
|
11
|
+
- operations
|
|
12
|
+
- ops
|
|
13
|
+
- sales
|
|
14
|
+
- support
|
|
15
|
+
- generalist
|
|
16
|
+
triggers:
|
|
17
|
+
- list canva designs
|
|
18
|
+
- find the canva design
|
|
19
|
+
- open in canva
|
|
20
|
+
- which canva design
|
|
21
|
+
tags:
|
|
22
|
+
- canva
|
|
23
|
+
- designs
|
|
24
|
+
execution:
|
|
25
|
+
type: script
|
|
26
|
+
script:
|
|
27
|
+
file: execute.sh
|
|
28
|
+
interpreter: bash
|
|
29
|
+
timeoutMs: 60000
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
# Canva Designs
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bash execute.sh --query "open day poster" --owned
|
|
36
|
+
bash execute.sh --id DAF…
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Output
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{"count":1,"designs":[{"id":"DAF…","title":"Open day poster","editUrl":"https://www.canva.com/…","viewUrl":"https://…","thumbnailUrl":"https://…","pageCount":1,"updatedAt":"…"}]}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`editUrl` / `viewUrl` are valid for 30 days, `thumbnailUrl` for 15 minutes —
|
|
46
|
+
share the edit link with people, do not store the thumbnail.
|
|
47
|
+
|
|
48
|
+
## Failures
|
|
49
|
+
|
|
50
|
+
`{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
|
|
51
|
+
when the owner has not connected Canva (Settings → Integrations).
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# canva-designs — List / search the owner's Canva designs, or fetch one (edit + view links)
|
|
4
|
+
#
|
|
5
|
+
# Backed by GET /api/canva/designs and GET /api/canva/designs/:id.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# bash execute.sh [--query "poster"] [--owned|--shared] [--sort modified_descending] [--limit 25]
|
|
9
|
+
# bash execute.sh --id <designId>
|
|
10
|
+
# bash execute.sh '{"query":"poster","limit":10}'
|
|
11
|
+
# =============================================================================
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
source "${SCRIPT_DIR}/../../_common/lib.sh"
|
|
15
|
+
|
|
16
|
+
print_usage() {
|
|
17
|
+
cat <<'EOF_USAGE'
|
|
18
|
+
Usage:
|
|
19
|
+
bash execute.sh [--query "poster"] [--owned|--shared] [--sort modified_descending] [--limit 25]
|
|
20
|
+
bash execute.sh --id <designId>
|
|
21
|
+
bash execute.sh '{"query":"poster","limit":10}'
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--query, -q Text to search design titles
|
|
25
|
+
--id Fetch one design instead of listing
|
|
26
|
+
--owned Only designs the owner created (default: any)
|
|
27
|
+
--shared Only designs shared with the owner
|
|
28
|
+
--sort relevance | modified_descending | modified_ascending | title_descending | title_ascending
|
|
29
|
+
--limit Result cap (default 25, max 100)
|
|
30
|
+
--continuation Token from a previous page
|
|
31
|
+
--help | -h Show this help
|
|
32
|
+
EOF_USAGE
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fail_from() {
|
|
36
|
+
printf '%s' "$1" | jq -c '{success: false, reason: (.details.error // .details // .error // "unknown"), hint: (.details.hint // ""), message: (.details.message // "")}' 2>/dev/null \
|
|
37
|
+
|| jq -n --arg r "$1" '{success: false, reason: $r}'
|
|
38
|
+
exit 1
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
uri() { jq -rn --arg v "$1" '$v|@uri'; }
|
|
42
|
+
|
|
43
|
+
# api_call may print a one-line warning to stderr (no CREWLY_SESSION_NAME);
|
|
44
|
+
# the backend answer is always the last line. On failure print the mapped
|
|
45
|
+
# failure JSON (fail_from) and return 1.
|
|
46
|
+
call() {
|
|
47
|
+
local out
|
|
48
|
+
out=$(api_call "$@" 2>&1) || { fail_from "$(printf '%s
|
|
49
|
+
' "$out" | tail -n 1)"; }
|
|
50
|
+
printf '%s
|
|
51
|
+
' "$out" | tail -n 1
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
INPUT_JSON=""
|
|
55
|
+
if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
|
|
56
|
+
INPUT_JSON="$1"
|
|
57
|
+
shift || true
|
|
58
|
+
fi
|
|
59
|
+
QUERY=""; ID=""; OWNERSHIP=""; SORT=""; LIMIT=""; CONT=""
|
|
60
|
+
while [[ $# -gt 0 ]]; do
|
|
61
|
+
case "$1" in
|
|
62
|
+
--query|-q) [ $# -ge 2 ] || error_exit "--query requires a value"; QUERY="$2"; shift 2 ;;
|
|
63
|
+
--id) [ $# -ge 2 ] || error_exit "--id requires a value"; ID="$2"; shift 2 ;;
|
|
64
|
+
--owned) OWNERSHIP="owned"; shift ;;
|
|
65
|
+
--shared) OWNERSHIP="shared"; shift ;;
|
|
66
|
+
--sort) [ $# -ge 2 ] || error_exit "--sort requires a value"; SORT="$2"; shift 2 ;;
|
|
67
|
+
--limit|-n) [ $# -ge 2 ] || error_exit "--limit requires a value"; LIMIT="$2"; shift 2 ;;
|
|
68
|
+
--continuation) [ $# -ge 2 ] || error_exit "--continuation requires a value"; CONT="$2"; shift 2 ;;
|
|
69
|
+
--help|-h) print_usage; exit 0 ;;
|
|
70
|
+
*) error_exit "Unknown option: $1" ;;
|
|
71
|
+
esac
|
|
72
|
+
done
|
|
73
|
+
if [ -n "$INPUT_JSON" ]; then
|
|
74
|
+
INPUT=$(read_json_input "$INPUT_JSON")
|
|
75
|
+
[ -z "$QUERY" ] && QUERY=$(printf '%s' "$INPUT" | jq -r '.query // .q // empty')
|
|
76
|
+
[ -z "$ID" ] && ID=$(printf '%s' "$INPUT" | jq -r '.id // .designId // empty')
|
|
77
|
+
[ -z "$OWNERSHIP" ] && OWNERSHIP=$(printf '%s' "$INPUT" | jq -r '.ownership // empty')
|
|
78
|
+
[ -z "$SORT" ] && SORT=$(printf '%s' "$INPUT" | jq -r '.sort // .sortBy // empty')
|
|
79
|
+
[ -z "$LIMIT" ] && LIMIT=$(printf '%s' "$INPUT" | jq -r '.limit // empty')
|
|
80
|
+
[ -z "$CONT" ] && CONT=$(printf '%s' "$INPUT" | jq -r '.continuation // empty')
|
|
81
|
+
fi
|
|
82
|
+
if [ -n "$ID" ]; then
|
|
83
|
+
RESPONSE=$(call GET "/canva/designs/$(uri "$ID")") || { printf '%s\n' "$RESPONSE"; exit 1; }
|
|
84
|
+
printf '%s' "$RESPONSE" | jq -c '.data'
|
|
85
|
+
exit 0
|
|
86
|
+
fi
|
|
87
|
+
QS=""
|
|
88
|
+
[ -n "$QUERY" ] && QS="${QS}&q=$(uri "$QUERY")"
|
|
89
|
+
[ -n "$OWNERSHIP" ] && QS="${QS}&ownership=$(uri "$OWNERSHIP")"
|
|
90
|
+
[ -n "$SORT" ] && QS="${QS}&sort=$(uri "$SORT")"
|
|
91
|
+
[ -n "$LIMIT" ] && QS="${QS}&limit=$(uri "$LIMIT")"
|
|
92
|
+
[ -n "$CONT" ] && QS="${QS}&continuation=$(uri "$CONT")"
|
|
93
|
+
QS="${QS#&}"
|
|
94
|
+
RESPONSE=$(call GET "/canva/designs${QS:+?$QS}") || { printf '%s\n' "$RESPONSE"; exit 1; }
|
|
95
|
+
if printf '%s' "$RESPONSE" | jq -e '.truncated == true' >/dev/null 2>&1; then printf '%s\n' "$RESPONSE"; exit 0; fi
|
|
96
|
+
printf '%s' "$RESPONSE" | jq -c '{count: .data.count, designs: .data.designs} + (if .data.continuation then {continuation: .data.continuation} else {} end)'
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for canva-designs — run with: bash execute.test.sh
|
|
3
|
+
# Python HTTP stub as the backend; asserts the request the skill sends and
|
|
4
|
+
# the JSON it prints. Exit 0 on pass.
|
|
5
|
+
set -u
|
|
6
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
EXEC="$HERE/execute.sh"
|
|
8
|
+
PASS=0; FAIL=0
|
|
9
|
+
check() {
|
|
10
|
+
local name="$1"; local got="$2"; local want="$3"
|
|
11
|
+
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
PORT=18820
|
|
15
|
+
STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
|
|
16
|
+
python3 -u <<'PY' >/dev/null 2>&1 &
|
|
17
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
18
|
+
import json, os
|
|
19
|
+
LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
|
|
20
|
+
class H(BaseHTTPRequestHandler):
|
|
21
|
+
def _reply(self, method, data=None):
|
|
22
|
+
open(LOG, 'w').write(json.dumps({'method': method, 'path': self.path, 'body': data}))
|
|
23
|
+
if 'no-grant' in self.path or (data or {}).get('title') == 'no-grant':
|
|
24
|
+
self.send_response(409); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
25
|
+
self.wfile.write(json.dumps({'success': False, 'error': 'not_connected', 'message': 'no grant', 'hint': 'https://cloud/start?token=j'}).encode()); return
|
|
26
|
+
self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
27
|
+
self.wfile.write(json.dumps({'success': True, 'data': {'count': 1, 'designs': [{'id': 'D1', 'title': 'Poster', 'editUrl': 'https://e'}], 'id': 'D1', 'title': 'Poster', 'editUrl': 'https://e'}}).encode())
|
|
28
|
+
def do_GET(self): self._reply('GET')
|
|
29
|
+
def do_POST(self):
|
|
30
|
+
n = int(self.headers.get('content-length', '0')); body = self.rfile.read(n).decode() if n else ''
|
|
31
|
+
self._reply('POST', json.loads(body or '{}'))
|
|
32
|
+
def log_message(self, *a, **k): pass
|
|
33
|
+
HTTPServer(('127.0.0.1', PORT), H).serve_forever()
|
|
34
|
+
PY
|
|
35
|
+
STUB_PID=$!
|
|
36
|
+
disown "$STUB_PID" 2>/dev/null || true
|
|
37
|
+
trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
|
|
38
|
+
for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
|
|
39
|
+
|
|
40
|
+
run() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
|
|
41
|
+
run_err() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null; }
|
|
42
|
+
|
|
43
|
+
OUT=$(run --query "open day" --owned --sort modified_descending --limit 5)
|
|
44
|
+
check "list: output" "$OUT" '{"count":1,"designs":[{"id":"D1","title":"Poster","editUrl":"https://e"}]}'
|
|
45
|
+
check "list: path" "$(jq -r .path "$STUB_LOG")" "/api/canva/designs?q=open%20day&ownership=owned&sort=modified_descending&limit=5"
|
|
46
|
+
run --id D1 >/dev/null
|
|
47
|
+
check "get: path" "$(jq -r .path "$STUB_LOG")" "/api/canva/designs/D1"
|
|
48
|
+
run '{"shared":false}' >/dev/null
|
|
49
|
+
check "json: bare list" "$(jq -r .path "$STUB_LOG")" "/api/canva/designs"
|
|
50
|
+
OUT=$(run --query no-grant || true)
|
|
51
|
+
check "not connected" "$(printf '%s' "$OUT" | jq -r .reason)" "not_connected"
|
|
52
|
+
|
|
53
|
+
echo "canva-designs: $PASS passed, $FAIL failed"
|
|
54
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Canva Export
|
|
3
|
+
description: Export a Canva design to PDF, PNG, JPG, PPTX, GIF or MP4 (via the Canva grant held by Crewly Cloud). Waits for Canva's export job and returns download URLs; with --out / --out-dir the files are downloaded. Use this to turn a finished Canva video/poster into a file you can post or send.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
category: productivity
|
|
6
|
+
skillType: claude-skill
|
|
7
|
+
assignableRoles:
|
|
8
|
+
- orchestrator
|
|
9
|
+
- team-leader
|
|
10
|
+
- developer
|
|
11
|
+
- operations
|
|
12
|
+
- ops
|
|
13
|
+
- sales
|
|
14
|
+
- support
|
|
15
|
+
- generalist
|
|
16
|
+
triggers:
|
|
17
|
+
- export from canva
|
|
18
|
+
- download the canva design
|
|
19
|
+
- render the canva video
|
|
20
|
+
- canva to pdf
|
|
21
|
+
tags:
|
|
22
|
+
- canva
|
|
23
|
+
- export
|
|
24
|
+
execution:
|
|
25
|
+
type: script
|
|
26
|
+
script:
|
|
27
|
+
file: execute.sh
|
|
28
|
+
interpreter: bash
|
|
29
|
+
timeoutMs: 60000
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
# Canva Export
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bash execute.sh --id DAF… --format mp4 --out /tmp/reel.mp4
|
|
36
|
+
bash execute.sh --id DAF… --format png --out-dir /tmp/poster-pages
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Output
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{"success":true,"jobId":"…","urls":["https://export-download.canva.com/…"],"savedTo":["/tmp/reel.mp4"]}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Download URLs are short-lived — save the file right away. Exports of
|
|
46
|
+
large videos can take a minute or two; the backend polls for up to 2 min
|
|
47
|
+
and answers `504` if Canva is still rendering (retry later).
|
|
48
|
+
|
|
49
|
+
## Failures
|
|
50
|
+
|
|
51
|
+
`{"success":false,"reason":"not_connected","hint":"<connect URL>"}` (exit 1)
|
|
52
|
+
when the owner has not connected Canva (Settings → Integrations).
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# canva-export — Export a Canva design to PDF / PNG / JPG / PPTX / GIF / MP4 and download it
|
|
4
|
+
#
|
|
5
|
+
# Backed by POST /api/canva/designs/:id/export (async job, polled by the backend).
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# bash execute.sh --id <designId> --format pdf [--out /tmp/poster.pdf]
|
|
9
|
+
# bash execute.sh --id <designId> --format mp4 [--video-quality horizontal_1080p] --out /tmp/reel.mp4
|
|
10
|
+
# bash execute.sh --id <designId> --format png --pages 1,2 --out-dir /tmp/pages
|
|
11
|
+
# bash execute.sh '{"id":"<designId>","format":"jpg","quality":85}'
|
|
12
|
+
# =============================================================================
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
source "${SCRIPT_DIR}/../../_common/lib.sh"
|
|
16
|
+
|
|
17
|
+
print_usage() {
|
|
18
|
+
cat <<'EOF_USAGE'
|
|
19
|
+
Usage:
|
|
20
|
+
bash execute.sh --id <designId> --format pdf [--out /tmp/poster.pdf]
|
|
21
|
+
bash execute.sh --id <designId> --format mp4 [--video-quality horizontal_1080p] --out /tmp/reel.mp4
|
|
22
|
+
bash execute.sh --id <designId> --format png --pages 1,2 --out-dir /tmp/pages
|
|
23
|
+
bash execute.sh '{"id":"<designId>","format":"jpg","quality":85}'
|
|
24
|
+
|
|
25
|
+
Options:
|
|
26
|
+
--id Design id (required)
|
|
27
|
+
--format pdf | png | jpg | pptx | gif | mp4 (required)
|
|
28
|
+
--quality JPG quality 1–100 (default 80)
|
|
29
|
+
--video-quality MP4 preset: horizontal_480p|720p|1080p|4k, vertical_… (default horizontal_1080p)
|
|
30
|
+
--pages Comma-separated 1-based page numbers (default all)
|
|
31
|
+
--out Download the (first) file to this path
|
|
32
|
+
--out-dir Download every file into this directory (page-1.png, page-2.png, …)
|
|
33
|
+
--help | -h Show this help
|
|
34
|
+
EOF_USAGE
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fail_from() {
|
|
38
|
+
printf '%s' "$1" | jq -c '{success: false, reason: (.details.error // .details // .error // "unknown"), hint: (.details.hint // ""), message: (.details.message // "")}' 2>/dev/null \
|
|
39
|
+
|| jq -n --arg r "$1" '{success: false, reason: $r}'
|
|
40
|
+
exit 1
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
uri() { jq -rn --arg v "$1" '$v|@uri'; }
|
|
44
|
+
|
|
45
|
+
# api_call may print a one-line warning to stderr (no CREWLY_SESSION_NAME);
|
|
46
|
+
# the backend answer is always the last line. On failure print the mapped
|
|
47
|
+
# failure JSON (fail_from) and return 1.
|
|
48
|
+
call() {
|
|
49
|
+
local out
|
|
50
|
+
out=$(api_call "$@" 2>&1) || { fail_from "$(printf '%s
|
|
51
|
+
' "$out" | tail -n 1)"; }
|
|
52
|
+
printf '%s
|
|
53
|
+
' "$out" | tail -n 1
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
INPUT_JSON=""
|
|
57
|
+
if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
|
|
58
|
+
INPUT_JSON="$1"
|
|
59
|
+
shift || true
|
|
60
|
+
fi
|
|
61
|
+
ID=""; FORMAT=""; QUALITY=""; VQ=""; PAGES=""; OUT_PATH=""; OUT_DIR=""
|
|
62
|
+
while [[ $# -gt 0 ]]; do
|
|
63
|
+
case "$1" in
|
|
64
|
+
--id) [ $# -ge 2 ] || error_exit "--id requires a value"; ID="$2"; shift 2 ;;
|
|
65
|
+
--format|-f) [ $# -ge 2 ] || error_exit "--format requires a value"; FORMAT="$2"; shift 2 ;;
|
|
66
|
+
--quality) [ $# -ge 2 ] || error_exit "--quality requires a value"; QUALITY="$2"; shift 2 ;;
|
|
67
|
+
--video-quality) [ $# -ge 2 ] || error_exit "--video-quality requires a value"; VQ="$2"; shift 2 ;;
|
|
68
|
+
--pages) [ $# -ge 2 ] || error_exit "--pages requires a value"; PAGES="$2"; shift 2 ;;
|
|
69
|
+
--out|-o) [ $# -ge 2 ] || error_exit "--out requires a value"; OUT_PATH="$2"; shift 2 ;;
|
|
70
|
+
--out-dir) [ $# -ge 2 ] || error_exit "--out-dir requires a value"; OUT_DIR="$2"; shift 2 ;;
|
|
71
|
+
--help|-h) print_usage; exit 0 ;;
|
|
72
|
+
*) error_exit "Unknown option: $1" ;;
|
|
73
|
+
esac
|
|
74
|
+
done
|
|
75
|
+
if [ -n "$INPUT_JSON" ]; then
|
|
76
|
+
INPUT=$(read_json_input "$INPUT_JSON")
|
|
77
|
+
[ -z "$ID" ] && ID=$(printf '%s' "$INPUT" | jq -r '.id // .designId // empty')
|
|
78
|
+
[ -z "$FORMAT" ] && FORMAT=$(printf '%s' "$INPUT" | jq -r '.format // empty')
|
|
79
|
+
[ -z "$QUALITY" ] && QUALITY=$(printf '%s' "$INPUT" | jq -r '.quality // empty')
|
|
80
|
+
[ -z "$VQ" ] && VQ=$(printf '%s' "$INPUT" | jq -r '.videoQuality // empty')
|
|
81
|
+
[ -z "$PAGES" ] && PAGES=$(printf '%s' "$INPUT" | jq -r '(.pages // []) | map(tostring) | join(",")')
|
|
82
|
+
[ -z "$OUT_PATH" ] && OUT_PATH=$(printf '%s' "$INPUT" | jq -r '.out // empty')
|
|
83
|
+
[ -z "$OUT_DIR" ] && OUT_DIR=$(printf '%s' "$INPUT" | jq -r '.outDir // empty')
|
|
84
|
+
fi
|
|
85
|
+
require_param "id (--id)" "$ID"
|
|
86
|
+
require_param "format (--format)" "$FORMAT"
|
|
87
|
+
BODY=$(jq -cn --arg format "$FORMAT" --arg q "$QUALITY" --arg vq "$VQ" --arg pages "$PAGES" \
|
|
88
|
+
'{format: $format}
|
|
89
|
+
+ (if $q != "" then {quality: ($q|tonumber)} else {} end)
|
|
90
|
+
+ (if $vq != "" then {videoQuality: $vq} else {} end)
|
|
91
|
+
+ (if $pages != "" then {pages: ($pages | split(",") | map(tonumber))} else {} end)')
|
|
92
|
+
RESPONSE=$(call POST "/canva/designs/$(uri "$ID")/export" "$BODY") || { printf '%s\n' "$RESPONSE"; exit 1; }
|
|
93
|
+
STATUS=$(printf '%s' "$RESPONSE" | jq -r '.data.status // "failed"')
|
|
94
|
+
if [ "$STATUS" != "success" ]; then
|
|
95
|
+
printf '%s' "$RESPONSE" | jq -c '{success: false, reason: "export_failed", status: .data.status, error: .data.error}'
|
|
96
|
+
exit 1
|
|
97
|
+
fi
|
|
98
|
+
SAVED="[]"
|
|
99
|
+
if [ -n "$OUT_PATH" ] || [ -n "$OUT_DIR" ]; then
|
|
100
|
+
i=0
|
|
101
|
+
while IFS= read -r url; do
|
|
102
|
+
[ -n "$url" ] || continue
|
|
103
|
+
i=$((i+1))
|
|
104
|
+
if [ -n "$OUT_PATH" ] && [ "$i" -eq 1 ]; then dest="$OUT_PATH"
|
|
105
|
+
elif [ -n "$OUT_DIR" ]; then mkdir -p "$OUT_DIR"; dest="$OUT_DIR/page-$i.$FORMAT"
|
|
106
|
+
else continue; fi
|
|
107
|
+
curl -sfL -o "$dest" "$url" || error_exit "download failed: $url"
|
|
108
|
+
SAVED=$(printf '%s' "$SAVED" | jq -c --arg d "$dest" '. + [$d]')
|
|
109
|
+
done < <(printf '%s' "$RESPONSE" | jq -r '.data.urls[]')
|
|
110
|
+
fi
|
|
111
|
+
printf '%s' "$RESPONSE" | jq -c --argjson saved "$SAVED" '{success: true, jobId: .data.jobId, urls: .data.urls, savedTo: $saved}'
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for canva-export — run with: bash execute.test.sh
|
|
3
|
+
# Python HTTP stub as the backend; asserts the request the skill sends and
|
|
4
|
+
# the JSON it prints. Exit 0 on pass.
|
|
5
|
+
set -u
|
|
6
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
EXEC="$HERE/execute.sh"
|
|
8
|
+
PASS=0; FAIL=0
|
|
9
|
+
check() {
|
|
10
|
+
local name="$1"; local got="$2"; local want="$3"
|
|
11
|
+
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
PORT=18822
|
|
15
|
+
STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
|
|
16
|
+
python3 -u <<'PY' >/dev/null 2>&1 &
|
|
17
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
18
|
+
import json, os
|
|
19
|
+
LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
|
|
20
|
+
class H(BaseHTTPRequestHandler):
|
|
21
|
+
def _reply(self, method, data=None):
|
|
22
|
+
open(LOG, 'w').write(json.dumps({'method': method, 'path': self.path, 'body': data}))
|
|
23
|
+
if 'no-grant' in self.path or (data or {}).get('title') == 'no-grant':
|
|
24
|
+
self.send_response(409); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
25
|
+
self.wfile.write(json.dumps({'success': False, 'error': 'not_connected', 'message': 'no grant', 'hint': 'https://cloud/start?token=j'}).encode()); return
|
|
26
|
+
self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
27
|
+
self.wfile.write(json.dumps({'success': True, 'data': {'jobId': 'J1', 'status': 'success', 'urls': ['http://127.0.0.1:%d/dl/1' % PORT]}}).encode())
|
|
28
|
+
def do_GET(self): self._reply('GET')
|
|
29
|
+
def do_POST(self):
|
|
30
|
+
n = int(self.headers.get('content-length', '0')); body = self.rfile.read(n).decode() if n else ''
|
|
31
|
+
self._reply('POST', json.loads(body or '{}'))
|
|
32
|
+
def log_message(self, *a, **k): pass
|
|
33
|
+
HTTPServer(('127.0.0.1', PORT), H).serve_forever()
|
|
34
|
+
PY
|
|
35
|
+
STUB_PID=$!
|
|
36
|
+
disown "$STUB_PID" 2>/dev/null || true
|
|
37
|
+
trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
|
|
38
|
+
for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
|
|
39
|
+
|
|
40
|
+
run() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
|
|
41
|
+
run_err() { CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null; }
|
|
42
|
+
|
|
43
|
+
OUT=$(run --id D1 --format pdf --pages 1,2)
|
|
44
|
+
check "pdf: output" "$OUT" "{\"success\":true,\"jobId\":\"J1\",\"urls\":[\"http://127.0.0.1:${PORT}/dl/1\"],\"savedTo\":[]}"
|
|
45
|
+
check "pdf: path+body" "$(jq -r '.path + " " + (.body|tojson)' "$STUB_LOG")" '/api/canva/designs/D1/export {"format":"pdf","pages":[1,2]}'
|
|
46
|
+
run '{"id":"D1","format":"jpg","quality":85}' >/dev/null
|
|
47
|
+
check "jpg: body" "$(jq -c .body "$STUB_LOG")" '{"format":"jpg","quality":85}'
|
|
48
|
+
TMP="$(mktemp).pdf"
|
|
49
|
+
OUT=$(run --id D1 --format pdf --out "$TMP")
|
|
50
|
+
check "download: saved" "$(printf '%s' "$OUT" | jq -r '.savedTo[0]')" "$TMP"
|
|
51
|
+
check "download: bytes" "$(cat "$TMP")" '{"success": true, "data": {"jobId": "J1", "status": "success", "urls": ["http://127.0.0.1:'"$PORT"'/dl/1"]}}'
|
|
52
|
+
rm -f "$TMP"
|
|
53
|
+
|
|
54
|
+
echo "canva-export: $PASS passed, $FAIL failed"
|
|
55
|
+
[ "$FAIL" -eq 0 ]
|