@renkosky/lark-fe-skills 0.1.0
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/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/lark-fe-skills.js +98 -0
- package/package.json +37 -0
- package/skills/lark-fe-task/README.md +445 -0
- package/skills/lark-fe-task/SKILL.md +178 -0
- package/skills/lark-fe-task/agents/openai.yaml +4 -0
- package/skills/lark-fe-task/config/repos.json +4 -0
- package/skills/lark-fe-task/references/task-breakdown-template.md +63 -0
- package/skills/lark-fe-task/scripts/config.sh +119 -0
- package/skills/lark-fe-task/scripts/list-configs.sh +39 -0
- package/skills/lark-fe-task/scripts/plan-lark-doc.sh +212 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
REPO_ROOT="${1:-}"
|
|
5
|
+
FE_PROJECT_PATHS="${2:-}"
|
|
6
|
+
OUTPUT_DIR="${3:-docs/frontend-tasks}"
|
|
7
|
+
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
8
|
+
REGISTRY_DIR="$SKILL_DIR/config"
|
|
9
|
+
REGISTRY_FILE="$REGISTRY_DIR/repos.json"
|
|
10
|
+
|
|
11
|
+
require_lark_cli() {
|
|
12
|
+
if ! command -v lark-cli >/dev/null 2>&1; then
|
|
13
|
+
cat >&2 <<'EOF'
|
|
14
|
+
Error: lark-cli is required but was not found in PATH.
|
|
15
|
+
|
|
16
|
+
Please install and configure Lark CLI before using lark-fe-task:
|
|
17
|
+
|
|
18
|
+
npm install -g @larksuite/cli
|
|
19
|
+
lark-cli config init
|
|
20
|
+
lark-cli auth login --recommend
|
|
21
|
+
|
|
22
|
+
Official lark-cli documentation:
|
|
23
|
+
https://github.com/larksuite/cli/tree/main
|
|
24
|
+
|
|
25
|
+
If your organization uses a different installation method, install lark-cli first,
|
|
26
|
+
then rerun this command.
|
|
27
|
+
EOF
|
|
28
|
+
exit 127
|
|
29
|
+
fi
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
ensure_local_config_gitignore() {
|
|
33
|
+
local repo_root="$1"
|
|
34
|
+
local gitignore_file="$repo_root/.gitignore"
|
|
35
|
+
local ignore_path=".lark-fe-task/"
|
|
36
|
+
|
|
37
|
+
touch "$gitignore_file"
|
|
38
|
+
if ! grep -Fxq "$ignore_path" "$gitignore_file"; then
|
|
39
|
+
{
|
|
40
|
+
if [[ -s "$gitignore_file" ]]; then
|
|
41
|
+
printf '\n'
|
|
42
|
+
fi
|
|
43
|
+
printf '# lark-fe-task local config\n'
|
|
44
|
+
printf '%s\n' "$ignore_path"
|
|
45
|
+
} >> "$gitignore_file"
|
|
46
|
+
fi
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if [[ -z "$REPO_ROOT" || -z "$FE_PROJECT_PATHS" ]]; then
|
|
50
|
+
echo "Usage: config.sh <repo-root> <fe-project-paths-comma-separated> [output-dir]" >&2
|
|
51
|
+
exit 2
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
require_lark_cli
|
|
55
|
+
|
|
56
|
+
if [[ ! -d "$REPO_ROOT" ]]; then
|
|
57
|
+
echo "Repo root does not exist: $REPO_ROOT" >&2
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
IFS=',' read -r -a FE_PATH_ARRAY <<< "$FE_PROJECT_PATHS"
|
|
62
|
+
for fe_path in "${FE_PATH_ARRAY[@]}"; do
|
|
63
|
+
if [[ -z "$fe_path" ]]; then
|
|
64
|
+
echo "FE project path list contains an empty entry" >&2
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
if [[ ! -d "$REPO_ROOT/$fe_path" ]]; then
|
|
68
|
+
echo "FE project path does not exist: $REPO_ROOT/$fe_path" >&2
|
|
69
|
+
exit 1
|
|
70
|
+
fi
|
|
71
|
+
done
|
|
72
|
+
|
|
73
|
+
CONFIG_DIR="$REPO_ROOT/.lark-fe-task"
|
|
74
|
+
CONFIG_FILE="$CONFIG_DIR/config.env"
|
|
75
|
+
mkdir -p "$CONFIG_DIR"
|
|
76
|
+
mkdir -p "$REGISTRY_DIR"
|
|
77
|
+
|
|
78
|
+
{
|
|
79
|
+
printf 'REPO_ROOT=%q\n' "$REPO_ROOT"
|
|
80
|
+
printf 'FE_PROJECT_PATHS=%q\n' "$FE_PROJECT_PATHS"
|
|
81
|
+
printf 'OUTPUT_DIR=%q\n' "$OUTPUT_DIR"
|
|
82
|
+
} > "$CONFIG_FILE"
|
|
83
|
+
ensure_local_config_gitignore "$REPO_ROOT"
|
|
84
|
+
|
|
85
|
+
/usr/bin/ruby -rjson -rfileutils -rtime -e '
|
|
86
|
+
registry_file, repo_root, fe_project_paths, output_dir = ARGV
|
|
87
|
+
now = Time.now.utc.iso8601
|
|
88
|
+
data =
|
|
89
|
+
if File.exist?(registry_file) && !File.read(registry_file).strip.empty?
|
|
90
|
+
JSON.parse(File.read(registry_file))
|
|
91
|
+
else
|
|
92
|
+
{ "version" => 1, "repos" => [] }
|
|
93
|
+
end
|
|
94
|
+
data["version"] ||= 1
|
|
95
|
+
data["repos"] ||= []
|
|
96
|
+
entry = {
|
|
97
|
+
"repoRoot" => repo_root,
|
|
98
|
+
"feProjectPaths" => fe_project_paths.split(","),
|
|
99
|
+
"outputDir" => output_dir,
|
|
100
|
+
"updatedAt" => now
|
|
101
|
+
}
|
|
102
|
+
index = data["repos"].find_index { |repo| repo["repoRoot"] == repo_root }
|
|
103
|
+
if index
|
|
104
|
+
data["repos"][index] = entry
|
|
105
|
+
else
|
|
106
|
+
data["repos"] << entry
|
|
107
|
+
end
|
|
108
|
+
FileUtils.mkdir_p(File.dirname(registry_file))
|
|
109
|
+
File.write(registry_file, JSON.pretty_generate(data) + "\n")
|
|
110
|
+
' "$REGISTRY_FILE" "$REPO_ROOT" "$FE_PROJECT_PATHS" "$OUTPUT_DIR"
|
|
111
|
+
|
|
112
|
+
cat <<EOF
|
|
113
|
+
Configured lark-fe-task.
|
|
114
|
+
CONFIG_FILE=$CONFIG_FILE
|
|
115
|
+
REGISTRY_FILE=$REGISTRY_FILE
|
|
116
|
+
REPO_ROOT=$REPO_ROOT
|
|
117
|
+
FE_PROJECT_PATHS=$FE_PROJECT_PATHS
|
|
118
|
+
OUTPUT_DIR=$OUTPUT_DIR
|
|
119
|
+
EOF
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
REGISTRY_FILE="$SKILL_DIR/config/repos.json"
|
|
6
|
+
|
|
7
|
+
if [[ ! -f "$REGISTRY_FILE" || ! -s "$REGISTRY_FILE" ]]; then
|
|
8
|
+
cat <<'EOF'
|
|
9
|
+
No lark-fe-task repositories are configured yet.
|
|
10
|
+
|
|
11
|
+
Use:
|
|
12
|
+
+config <repo-root> <fe-project-paths> [output-dir]
|
|
13
|
+
EOF
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
/usr/bin/ruby -rjson -e '
|
|
18
|
+
registry_file = ARGV[0]
|
|
19
|
+
data = JSON.parse(File.read(registry_file))
|
|
20
|
+
repos = data["repos"] || []
|
|
21
|
+
|
|
22
|
+
if repos.empty?
|
|
23
|
+
puts "No lark-fe-task repositories are configured yet."
|
|
24
|
+
puts
|
|
25
|
+
puts "Use:"
|
|
26
|
+
puts " +config <repo-root> <fe-project-paths> [output-dir]"
|
|
27
|
+
exit 0
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
puts "Configured lark-fe-task repositories:"
|
|
31
|
+
repos.each_with_index do |repo, index|
|
|
32
|
+
fe_paths = Array(repo["feProjectPaths"]).join(",")
|
|
33
|
+
puts
|
|
34
|
+
puts "#{index + 1}. #{repo["repoRoot"]}"
|
|
35
|
+
puts " FE_PROJECT_PATHS=#{fe_paths}"
|
|
36
|
+
puts " OUTPUT_DIR=#{repo["outputDir"]}"
|
|
37
|
+
puts " UPDATED_AT=#{repo["updatedAt"]}"
|
|
38
|
+
end
|
|
39
|
+
' "$REGISTRY_FILE"
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
DOC_INPUT="${1:-}"
|
|
5
|
+
REPO_ROOT_ARG="${2:-}"
|
|
6
|
+
REPO_ROOT="$(pwd)"
|
|
7
|
+
FE_PROJECT_PATHS="<not-configured>"
|
|
8
|
+
OUTPUT_DIR="docs/frontend-tasks"
|
|
9
|
+
CONFIG_STATUS="missing"
|
|
10
|
+
CONFIG_SOURCE="<none>"
|
|
11
|
+
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
12
|
+
REGISTRY_FILE="$SKILL_DIR/config/repos.json"
|
|
13
|
+
|
|
14
|
+
require_lark_cli() {
|
|
15
|
+
if ! command -v lark-cli >/dev/null 2>&1; then
|
|
16
|
+
cat >&2 <<'EOF'
|
|
17
|
+
Error: lark-cli is required but was not found in PATH.
|
|
18
|
+
|
|
19
|
+
Please install and configure Lark CLI before using lark-fe-task:
|
|
20
|
+
|
|
21
|
+
npm install -g @larksuite/cli
|
|
22
|
+
lark-cli config init
|
|
23
|
+
lark-cli auth login --recommend
|
|
24
|
+
|
|
25
|
+
Official lark-cli documentation:
|
|
26
|
+
https://github.com/larksuite/cli/tree/main
|
|
27
|
+
|
|
28
|
+
If your organization uses a different installation method, install lark-cli first,
|
|
29
|
+
then rerun this command.
|
|
30
|
+
EOF
|
|
31
|
+
exit 127
|
|
32
|
+
fi
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if [[ -z "$DOC_INPUT" ]]; then
|
|
36
|
+
echo "Usage: plan-lark-doc.sh <lark-doc-title-or-url-or-token> [repo-root]" >&2
|
|
37
|
+
exit 2
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
require_lark_cli
|
|
41
|
+
|
|
42
|
+
find_config_upwards() {
|
|
43
|
+
local dir="$1"
|
|
44
|
+
while [[ "$dir" != "/" ]]; do
|
|
45
|
+
if [[ -f "$dir/.lark-fe-task/config.env" ]]; then
|
|
46
|
+
printf '%s\n' "$dir/.lark-fe-task/config.env"
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
dir="$(dirname "$dir")"
|
|
50
|
+
done
|
|
51
|
+
return 1
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
load_registry_config() {
|
|
55
|
+
if [[ ! -f "$REGISTRY_FILE" || ! -s "$REGISTRY_FILE" ]]; then
|
|
56
|
+
return 1
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
local registry_output
|
|
60
|
+
local registry_status
|
|
61
|
+
set +e
|
|
62
|
+
registry_output="$(/usr/bin/ruby -rjson -rshellwords -e '
|
|
63
|
+
data = JSON.parse(File.read(ARGV[0]))
|
|
64
|
+
repos = data["repos"] || []
|
|
65
|
+
if repos.empty?
|
|
66
|
+
exit 1
|
|
67
|
+
elsif repos.length == 1
|
|
68
|
+
repo = repos.first
|
|
69
|
+
puts "REPO_ROOT=#{Shellwords.escape(repo["repoRoot"])}"
|
|
70
|
+
puts "FE_PROJECT_PATHS=#{Shellwords.escape(Array(repo["feProjectPaths"]).join(","))}"
|
|
71
|
+
puts "OUTPUT_DIR=#{Shellwords.escape(repo["outputDir"] || "docs/frontend-tasks")}"
|
|
72
|
+
puts "CONFIG_STATUS=registry"
|
|
73
|
+
puts "CONFIG_SOURCE=#{Shellwords.escape(ARGV[0])}"
|
|
74
|
+
else
|
|
75
|
+
warn "Multiple lark-fe-task repositories are configured. Choose one and rerun +plan with its repo root:"
|
|
76
|
+
repos.each_with_index do |repo, index|
|
|
77
|
+
warn "#{index + 1}. #{repo["repoRoot"]} | FE_PROJECT_PATHS=#{Array(repo["feProjectPaths"]).join(",")} | OUTPUT_DIR=#{repo["outputDir"]}"
|
|
78
|
+
end
|
|
79
|
+
exit 3
|
|
80
|
+
end
|
|
81
|
+
' "$REGISTRY_FILE")"
|
|
82
|
+
registry_status=$?
|
|
83
|
+
set -e
|
|
84
|
+
|
|
85
|
+
if [[ "$registry_status" -eq 3 ]]; then
|
|
86
|
+
exit 3
|
|
87
|
+
fi
|
|
88
|
+
if [[ "$registry_status" -ne 0 ]]; then
|
|
89
|
+
return 1
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
eval "$registry_output"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if [[ -n "$REPO_ROOT_ARG" ]]; then
|
|
96
|
+
REPO_ROOT="$REPO_ROOT_ARG"
|
|
97
|
+
CONFIG_FILE="$REPO_ROOT/.lark-fe-task/config.env"
|
|
98
|
+
if [[ -f "$CONFIG_FILE" ]]; then
|
|
99
|
+
# shellcheck disable=SC1090
|
|
100
|
+
source "$CONFIG_FILE"
|
|
101
|
+
CONFIG_STATUS="loaded"
|
|
102
|
+
CONFIG_SOURCE="$CONFIG_FILE"
|
|
103
|
+
fi
|
|
104
|
+
else
|
|
105
|
+
CONFIG_FILE="$(find_config_upwards "$REPO_ROOT" || true)"
|
|
106
|
+
if [[ -n "$CONFIG_FILE" ]]; then
|
|
107
|
+
# shellcheck disable=SC1090
|
|
108
|
+
source "$CONFIG_FILE"
|
|
109
|
+
CONFIG_STATUS="loaded"
|
|
110
|
+
CONFIG_SOURCE="$CONFIG_FILE"
|
|
111
|
+
else
|
|
112
|
+
load_registry_config || true
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
is_url_or_token() {
|
|
117
|
+
[[ "$DOC_INPUT" =~ ^https?:// ]] || [[ "$DOC_INPUT" =~ ^[A-Za-z0-9]{20,}$ ]]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
slugify() {
|
|
121
|
+
local raw="$1"
|
|
122
|
+
local slug
|
|
123
|
+
slug="$(printf '%s' "$raw" | /usr/bin/ruby -EUTF-8 -e '
|
|
124
|
+
raw = STDIN.read
|
|
125
|
+
issue = raw.match(/(?:PR|pr)[-_[:space:]]*([0-9]{3,})/)
|
|
126
|
+
if issue
|
|
127
|
+
puts "pr-#{issue[1]}"
|
|
128
|
+
exit
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
slug = raw.downcase
|
|
132
|
+
.gsub(/\[[^\]]*\]/, "")
|
|
133
|
+
.gsub(/[^a-z0-9]+/, "-")
|
|
134
|
+
.gsub(/^-+|-+$/, "")
|
|
135
|
+
.gsub(/-+/, "-")
|
|
136
|
+
puts slug
|
|
137
|
+
')"
|
|
138
|
+
if [[ -z "$slug" ]]; then
|
|
139
|
+
slug="lark-fe-task"
|
|
140
|
+
fi
|
|
141
|
+
printf '%s' "$slug"
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
ensure_output_gitignore() {
|
|
145
|
+
local repo_root="$1"
|
|
146
|
+
local output_dir="$2"
|
|
147
|
+
local gitignore_file="$repo_root/.gitignore"
|
|
148
|
+
local ignore_path="$output_dir"
|
|
149
|
+
|
|
150
|
+
if [[ "$ignore_path" = "$repo_root/"* ]]; then
|
|
151
|
+
ignore_path="${ignore_path#"$repo_root/"}"
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
ignore_path="${ignore_path#/}"
|
|
155
|
+
ignore_path="${ignore_path%/}/"
|
|
156
|
+
|
|
157
|
+
if [[ -z "$ignore_path" || "$ignore_path" == "../"* || "$ignore_path" == /* ]]; then
|
|
158
|
+
return 0
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
touch "$gitignore_file"
|
|
162
|
+
if ! grep -Fxq "$ignore_path" "$gitignore_file"; then
|
|
163
|
+
{
|
|
164
|
+
if [[ -s "$gitignore_file" ]]; then
|
|
165
|
+
printf '\n'
|
|
166
|
+
fi
|
|
167
|
+
printf '# lark-fe-task generated task breakdowns\n'
|
|
168
|
+
printf '%s\n' "$ignore_path"
|
|
169
|
+
} >> "$gitignore_file"
|
|
170
|
+
fi
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
DOC_REF="$DOC_INPUT"
|
|
174
|
+
TITLE="$DOC_INPUT"
|
|
175
|
+
|
|
176
|
+
if ! is_url_or_token; then
|
|
177
|
+
SEARCH_JSON="$(lark-cli drive +search --as user --query "$DOC_INPUT" --doc-types docx,doc,wiki --page-size 20 --format json)"
|
|
178
|
+
DOC_REF="$(printf '%s' "$SEARCH_JSON" | /usr/bin/ruby -rjson -e '
|
|
179
|
+
data = JSON.parse(STDIN.read).dig("data", "results") || []
|
|
180
|
+
exact = data.find { |r| (r.dig("title_highlighted") || "").gsub(/<\/?h>/, "").gsub(/<\/?hb>/, "") == ARGV[0] }
|
|
181
|
+
chosen = exact || data.first
|
|
182
|
+
abort("No Lark document found") unless chosen
|
|
183
|
+
puts chosen.dig("result_meta", "url") || chosen.dig("result_meta", "token")
|
|
184
|
+
' "$DOC_INPUT")"
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
FETCH_JSON="$(lark-cli docs +fetch --as user --api-version v2 --doc "$DOC_REF" --doc-format markdown)"
|
|
188
|
+
CONTENT="$(printf '%s' "$FETCH_JSON" | /usr/bin/ruby -rjson -e 'puts JSON.parse(STDIN.read).dig("data", "document", "content")')"
|
|
189
|
+
DOC_TITLE="$(printf '%s' "$CONTENT" | sed -n 's/^<title>\(.*\)<\/title>$/\1/p; s/^# \(.*\)$/\1/p' | sed -n '1p')"
|
|
190
|
+
|
|
191
|
+
if [[ -n "$DOC_TITLE" ]]; then
|
|
192
|
+
TITLE="$DOC_TITLE"
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
OUT_DIR="$REPO_ROOT/$OUTPUT_DIR"
|
|
196
|
+
OUT_FILE="$OUT_DIR/$(date +%F)-$(slugify "$TITLE").md"
|
|
197
|
+
ensure_output_gitignore "$REPO_ROOT" "$OUTPUT_DIR"
|
|
198
|
+
|
|
199
|
+
cat <<EOF
|
|
200
|
+
DOCUMENT_REF=$DOC_REF
|
|
201
|
+
REPO_ROOT=$REPO_ROOT
|
|
202
|
+
CONFIG_STATUS=$CONFIG_STATUS
|
|
203
|
+
CONFIG_SOURCE=$CONFIG_SOURCE
|
|
204
|
+
FE_PROJECT_PATHS=$FE_PROJECT_PATHS
|
|
205
|
+
OUTPUT_PATH=$OUT_FILE
|
|
206
|
+
|
|
207
|
+
--- LARK_DOC_MARKDOWN_BEGIN ---
|
|
208
|
+
$CONTENT
|
|
209
|
+
--- LARK_DOC_MARKDOWN_END ---
|
|
210
|
+
|
|
211
|
+
Next: choose the affected FE project path from FE_PROJECT_PATHS, inspect it, use references/task-breakdown-template.md, then write the final task breakdown to OUTPUT_PATH.
|
|
212
|
+
EOF
|