diff --git a/.github/workflows/install-scripts.yml b/.github/workflows/install-scripts.yml index 889d13c..3dbdc80 100644 --- a/.github/workflows/install-scripts.yml +++ b/.github/workflows/install-scripts.yml @@ -8,12 +8,14 @@ on: - install.sh - install.ps1 - tests/install_sh_sudo_test.sh + - tests/install_ps1_test.ps1 - .github/workflows/install-scripts.yml pull_request: paths: - install.sh - install.ps1 - tests/install_sh_sudo_test.sh + - tests/install_ps1_test.ps1 - .github/workflows/install-scripts.yml permissions: @@ -34,6 +36,23 @@ jobs: - name: installer behavior tests run: sh tests/install_sh_sudo_test.sh + windows: + name: install.ps1 behavior (windows) + runs-on: windows-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 + with: + go-version-file: "go.mod" + cache: false + - name: installer behavior tests (Windows PowerShell 5.1) + shell: powershell + run: ./tests/install_ps1_test.ps1 + - name: installer behavior tests (PowerShell 7) + if: ${{ !cancelled() }} + shell: pwsh + run: ./tests/install_ps1_test.ps1 + mirror: name: mirror install scripts runs-on: ubuntu-latest diff --git a/cmd/flashduty/main.go b/cmd/flashduty/main.go index 9a7711c..d9128ab 100644 --- a/cmd/flashduty/main.go +++ b/cmd/flashduty/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "path/filepath" "runtime/debug" "strings" @@ -29,7 +30,17 @@ func main() { date = "unknown" } cli.SetVersionInfo(version, commit, date) - if err := cli.Execute(); err != nil { + // The CLI names itself after the command word the user typed, so a copy + // installed under another name (install.sh INSTALLED_NAME) shows that name + // in help, errors and completion scripts. + name := filepath.Base(os.Args[0]) + if ext := filepath.Ext(name); strings.EqualFold(ext, ".exe") { + name = strings.TrimSuffix(name, ext) + } + if name == "" || name == "." || name == string(filepath.Separator) { + name = "flashduty" + } + if err := cli.Execute(name); err != nil { fmt.Fprintf(os.Stderr, "Error: %s\n", err) os.Exit(1) } diff --git a/install.ps1 b/install.ps1 index cc9e418..fde7207 100644 --- a/install.ps1 +++ b/install.ps1 @@ -4,6 +4,7 @@ # Environment variables: # FLASHDUTY_VERSION - specific version to install (e.g. "v0.1.2") # FLASHDUTY_INSTALL_DIR - install directory (default: $HOME\.flashduty\bin) +# INSTALLED_NAME - installed command name (default: flashduty) # MIRROR_URL - fetch release assets from this https mirror prefix. # Default: https://static.flashcat.cloud/flashduty-cli. # The mirror must replicate @@ -17,7 +18,8 @@ $ErrorActionPreference = "Stop" $Repo = "flashcatcloud/flashduty-cli" $Binary = "flashduty-cli.exe" -$InstalledName = "flashduty.exe" +$CommandName = if ($env:INSTALLED_NAME) { $env:INSTALLED_NAME -replace '\.exe$', '' } else { "flashduty" } +$InstalledName = "$CommandName.exe" # By default release downloads are fetched from the Flashcat CDN. Set MIRROR_URL # to another prefix to override, or to an empty string to force GitHub fallback. @@ -153,6 +155,15 @@ try { } $DestPath = Join-Path $InstallDir $InstalledName + if (Test-Path $DestPath) { + # A running .exe can't be overwritten or deleted but can be renamed: + # move it aside under a unique name so `update` can replace the binary + # it is running from. Copies moved aside earlier are deleted; one that + # is still running stays locked and is left for a later install. + Get-ChildItem -Path $InstallDir -Filter "$InstalledName.*.old" -File | + Remove-Item -Force -ErrorAction SilentlyContinue + Move-Item -Path $DestPath -Destination "$DestPath.$([System.Guid]::NewGuid().ToString('N')).old" + } Move-Item -Path $BinaryPath -Destination $DestPath -Force Write-Info "Installed to $DestPath" @@ -165,7 +176,7 @@ try { Write-Info "Added $InstallDir to user PATH (restart your terminal for it to take effect)" } - Write-Info "Run 'flashduty version' to verify" + Write-Info "Run '$CommandName version' to verify" } finally { Remove-Item -Path $TmpDir -Recurse -Force -ErrorAction SilentlyContinue } diff --git a/install.sh b/install.sh index c3d1b6e..ee7527e 100755 --- a/install.sh +++ b/install.sh @@ -5,6 +5,7 @@ # Environment: # FLASHDUTY_VERSION Install a specific version (e.g. v0.1.2). Default: latest. # FLASHDUTY_INSTALL_DIR Install directory. Default: /usr/local/bin. +# INSTALLED_NAME Installed command name. Default: flashduty. # MIRROR_URL Fetch release assets from this https mirror prefix. # Default: https://static.flashcat.cloud/flashduty-cli. # The mirror must replicate @@ -134,17 +135,6 @@ detect_shell() { esac } -# Emit the completion script for the shell named in $1. Cobra bakes the root -# command name "flashduty" into the script (#compdef / complete -c / function -# names); when installed under a different name, rewrite every occurrence so the -# completion binds to the actual command (the runtime dispatch line already uses -# the typed command word, so it needs no rewrite). The `|` sed delimiter is safe -# because a binary name can't contain it, and the rewrite is a no-op for the -# default "flashduty". -gen_completion() { - "${BIN}" completion "$1" | sed "s|flashduty|${INSTALLED_NAME}|g" -} - # Install completion for the current shell into a directory the shell already # auto-loads, without ever editing the user's rc files. zsh has no guaranteed # writable fpath dir, so it only succeeds when a standard site-functions dir is @@ -161,7 +151,7 @@ setup_completion() { dir="${XDG_CONFIG_HOME:-${HOME}/.config}/fish/completions" mkdir -p "${dir}" 2>/dev/null || true if [ -w "${dir}" ]; then - gen_completion fish > "${dir}/${INSTALLED_NAME}.fish" && { + "${BIN}" completion fish > "${dir}/${INSTALLED_NAME}.fish" && { info "Installed fish completion to ${dir}/${INSTALLED_NAME}.fish (restart fish to load)" return 0 } @@ -171,7 +161,7 @@ setup_completion() { dir="${XDG_DATA_HOME:-${HOME}/.local/share}/bash-completion/completions" mkdir -p "${dir}" 2>/dev/null || true if [ -w "${dir}" ]; then - gen_completion bash > "${dir}/${INSTALLED_NAME}" && { + "${BIN}" completion bash > "${dir}/${INSTALLED_NAME}" && { info "Installed bash completion to ${dir}/${INSTALLED_NAME} (needs the bash-completion package; restart bash to load)" return 0 } @@ -183,7 +173,7 @@ setup_completion() { "/usr/local/share/zsh/site-functions" \ "/usr/share/zsh/site-functions"; do if [ -d "${dir}" ] && [ -w "${dir}" ]; then - gen_completion zsh > "${dir}/_${INSTALLED_NAME}" && { + "${BIN}" completion zsh > "${dir}/_${INSTALLED_NAME}" && { info "Installed zsh completion to ${dir}/_${INSTALLED_NAME}" info " Run 'rm -f ~/.zcompdump*' and restart zsh to load." return 0 diff --git a/internal/cli/audit.go b/internal/cli/audit.go index 5c586b4..127846e 100644 --- a/internal/cli/audit.go +++ b/internal/cli/audit.go @@ -104,7 +104,7 @@ func newAuditSearchCmd() *cobra.Command { cmd.Flags().StringVar(&since, "since", "7d", "Start time") cmd.Flags().StringVar(&until, "until", "now", "End time") cmd.Flags().Int64Var(&person, "person", 0, "Filter by person ID") - cmd.Flags().StringVar(&operation, "operation", "", "Filter by exact operation name(s) from 'fduty audit operation-list' (e.g. monitRule:write:update); comma-separate to match several in one call. Prefixes do NOT match (\"monitRule\" returns nothing).") + cmd.Flags().StringVar(&operation, "operation", "", "Filter by exact operation name(s) from 'flashduty audit operation-list' (e.g. monitRule:write:update); comma-separate to match several in one call. Prefixes do NOT match (\"monitRule\" returns nothing).") cmd.Flags().IntVar(&limit, "limit", 20, "Max results (max 99)") cmd.Flags().IntVar(&page, "page", 1, "Page number") diff --git a/internal/cli/incident.go b/internal/cli/incident.go index 589ee0e..cd7851f 100644 --- a/internal/cli/incident.go +++ b/internal/cli/incident.go @@ -80,7 +80,7 @@ func newIncidentListCmd() *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "List incidents", - Long: curatedLong("List incidents matching the given filters. The --since/--until window must be < 31 days; --limit max is 100. In json/toon mode, rows default to the compact fields incident_id,num,title,incident_severity,progress,start_time,channel_id,detail_url; pass --fields to choose a different projection.\n\nSee also: fduty insight for aggregated metrics (MTTA, MTTR, noise reduction), fduty insight incident-list for metric-rich filtered incident rows, and fduty insight incident-export for CSV incident exports.", "Incidents", "List"), + Long: curatedLong("List incidents matching the given filters. The --since/--until window must be < 31 days; --limit max is 100. In json/toon mode, rows default to the compact fields incident_id,num,title,incident_severity,progress,start_time,channel_id,detail_url; pass --fields to choose a different projection.\n\nSee also: flashduty insight for aggregated metrics (MTTA, MTTR, noise reduction), flashduty insight incident-list for metric-rich filtered incident rows, and flashduty insight incident-export for CSV incident exports.", "Incidents", "List"), RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { startTime, err := timeutil.Parse(since) @@ -1004,7 +1004,7 @@ const maxIncidentVerifyConcurrency = 8 // symbol instead of pinning a copy of the sentence: reword this constant and // every caller (production and test) picks up the new wording automatically, // with nothing left to fall out of sync. -const commentVerificationGuidance = "The write API already reported success for the whole batch (a single POST covering every requested incident), and not finding a match here does not mean a comment is missing — this check can only confirm presence, never confirm absence. Do not write the comment again, for the full batch or for the incident(s) listed above alone: either risks a duplicate on a write that most likely already landed. Run `fduty incident timeline ` on the listed incident(s) to check by hand before deciding on anything further" +const commentVerificationGuidance = "The write API already reported success for the whole batch (a single POST covering every requested incident), and not finding a match here does not mean a comment is missing — this check can only confirm presence, never confirm absence. Do not write the comment again, for the full batch or for the incident(s) listed above alone: either risks a duplicate on a write that most likely already landed. Run `flashduty incident timeline ` on the listed incident(s) to check by hand before deciding on anything further" // commentVerificationNotFoundDetailFmt is the per-incident detail appended to // commentVerificationGuidance's problem list when the page walk completes diff --git a/internal/cli/incident_summary_script_test.go b/internal/cli/incident_summary_script_test.go index 6a15337..20f1589 100644 --- a/internal/cli/incident_summary_script_test.go +++ b/internal/cli/incident_summary_script_test.go @@ -19,12 +19,12 @@ func TestIncidentSummaryScriptCompactOutput(t *testing.T) { t.Fatalf("resolve repository root: %v", err) } script := filepath.Join(root, "skills", "flashduty", "scripts", "incident-summary.sh") - log := filepath.Join(t.TempDir(), "fduty.log") - bin := filepath.Join(t.TempDir(), "fduty") - if err := os.WriteFile(bin, []byte("#!/usr/bin/env bash\nprintf '%s\\n' \"$*\" >> \"$FDUTY_LOG\"\nprintf 'compact result\\n'\n"), 0o755); err != nil { - t.Fatalf("write fake fduty: %v", err) + log := filepath.Join(t.TempDir(), "flashduty.log") + bin := filepath.Join(t.TempDir(), "flashduty") + if err := os.WriteFile(bin, []byte("#!/usr/bin/env bash\nprintf '%s\\n' \"$*\" >> \"$FAKE_FLASHDUTY_LOG\"\nprintf 'compact result\\n'\n"), 0o755); err != nil { + t.Fatalf("write fake flashduty: %v", err) } - t.Setenv("FDUTY_LOG", log) + t.Setenv("FAKE_FLASHDUTY_LOG", log) t.Setenv("PATH", filepath.Dir(bin)+string(os.PathListSeparator)+os.Getenv("PATH")) output, err := exec.Command("bash", script, "inc-1").CombinedOutput() @@ -33,11 +33,11 @@ func TestIncidentSummaryScriptCompactOutput(t *testing.T) { } invocations, err := os.ReadFile(log) if err != nil { - t.Fatalf("read fake fduty log: %v", err) + t.Fatalf("read fake flashduty log: %v", err) } lines := strings.FieldsFunc(strings.TrimSpace(string(invocations)), func(r rune) bool { return r == '\n' }) if len(lines) != 7 { - t.Fatalf("fduty calls = %d, want 7 (six reads + the start_time probe):\n%s", len(lines), invocations) + t.Fatalf("flashduty calls = %d, want 7 (six reads + the start_time probe):\n%s", len(lines), invocations) } wantDetail := "incident detail inc-1 --fields incident_id,num,title,incident_severity,progress,ai_summary,root_cause,resolution,alert_cnt,start_time,channel_id,detail_url --output-format toon" if lines[0] != wantDetail { diff --git a/internal/cli/incident_test.go b/internal/cli/incident_test.go index e4eec5a..0c9f7b5 100644 --- a/internal/cli/incident_test.go +++ b/internal/cli/incident_test.go @@ -106,7 +106,7 @@ func TestCommandIncidentListHelpSurfacesInsightIncidentExport(t *testing.T) { if err != nil { t.Fatalf("incident list --help: %v", err) } - if !strings.Contains(out, "fduty insight incident-export") { + if !strings.Contains(out, "flashduty insight incident-export") { t.Fatalf("help output missing incident export discovery hint:\n%s", out) } } diff --git a/internal/cli/oncall.go b/internal/cli/oncall.go index 2f7a99e..fbcc7e2 100644 --- a/internal/cli/oncall.go +++ b/internal/cli/oncall.go @@ -33,7 +33,7 @@ func newOncallWhoCmd() *cobra.Command { cmd := &cobra.Command{ Use: "who", Short: "Show who is currently on call", - Long: curatedLong("Show who is currently on call across schedules within a time window, optionally filtered by team or schedule name. The table output already resolves person_ids to display names; when you have raw person_ids elsewhere, batch-resolve them with 'fduty person infos ...' (NOT by paginating 'fduty member list' — person_id and member_id are different id namespaces).", "Schedules", "List"), + Long: curatedLong("Show who is currently on call across schedules within a time window, optionally filtered by team or schedule name. The table output already resolves person_ids to display names; when you have raw person_ids elsewhere, batch-resolve them with 'flashduty person infos ...' (NOT by paginating 'flashduty member list' — person_id and member_id are different id namespaces).", "Schedules", "List"), RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { startTime, err := timeutil.Parse(since) diff --git a/internal/cli/root.go b/internal/cli/root.go index 845289d..d89a676 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -3,14 +3,17 @@ package cli import ( "context" "encoding/json" + "errors" "fmt" "io" "os" + "regexp" "strconv" "strings" "github.com/flashcatcloud/go-flashduty" "github.com/spf13/cobra" + "github.com/spf13/pflag" toon "github.com/toon-format/toon-go" "golang.org/x/term" @@ -57,7 +60,7 @@ var rootCmd = &cobra.Command{ } updateNotice = nil updateCheckWarning = "" - if cmd.CommandPath() == "flashduty update" { + if cmd.CommandPath() == cmd.Root().Name()+" update" { return nil } if update.IsManagedByRunner() { @@ -70,7 +73,7 @@ var rootCmd = &cobra.Command{ result, err := checkForUpdateAutoFn(versionStr) if err != nil { if update.IsTimeout(err) { - updateCheckWarning = "auto update check timeout, please run 'flashduty update --check' manually" + updateCheckWarning = "auto update check timeout, please run '" + cmd.Root().Name() + " update --check' manually" } else { updateNotice = update.StateHasUpdate(versionStr) } @@ -89,9 +92,9 @@ var rootCmd = &cobra.Command{ _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "\n%s\n", updateCheckWarning) } if updateNotice != nil { - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "\nA new version of flashduty is available: v%s -> %s\n", - update.StripV(updateNotice.CurrentVersion), updateNotice.LatestVersion) - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "To update, run: flashduty update\n") + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "\nA new version of %s is available: v%s -> %s\n", + cmd.Root().Name(), update.StripV(updateNotice.CurrentVersion), updateNotice.LatestVersion) + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "To update, run: %s update\n", cmd.Root().Name()) } }, } @@ -162,9 +165,44 @@ func init() { attachSafariSkillUpload(rootCmd) } -// Execute runs the root command. -func Execute() error { - return rootCmd.Execute() +// Execute runs the root command as name, the command word the binary was +// invoked as. Help and error text is authored with the canonical "flashduty"; +// under any other name the tree's text and the returned error are rewritten +// to it, and cobra derives completion scripts from the root's name. +func Execute(name string) error { + if name == rootCmd.Name() { + return rootCmd.Execute() + } + renameCommandText(rootCmd, name) + rootCmd.Use = name + if err := rootCmd.Execute(); err != nil { + return errors.New(renameCLI(err.Error(), name)) + } + return nil +} + +// renameCommandText rewrites the CLI name in cmd's help text and flag usages, +// recursively. +func renameCommandText(cmd *cobra.Command, name string) { + cmd.Short = renameCLI(cmd.Short, name) + cmd.Long = renameCLI(cmd.Long, name) + cmd.Example = renameCLI(cmd.Example, name) + rename := func(f *pflag.Flag) { f.Usage = renameCLI(f.Usage, name) } + cmd.Flags().VisitAll(rename) + cmd.PersistentFlags().VisitAll(rename) + for _, c := range cmd.Commands() { + renameCommandText(c, name) + } +} + +// cliWord matches "flashduty" used as a command word: followed by a space, and +// not part of a path, domain or longer identifier ("go-flashduty", +// "~/.flashduty"). +var cliWord = regexp.MustCompile(`(^|[^\w./~-])flashduty `) + +// renameCLI replaces every command-word "flashduty" in s with name. +func renameCLI(s, name string) string { + return cliWord.ReplaceAllString(s, "${1}"+strings.ReplaceAll(name, "$", "$$")+" ") } // newClient creates a go-flashduty client using the current factory. diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go new file mode 100644 index 0000000..a1e04c5 --- /dev/null +++ b/internal/cli/root_test.go @@ -0,0 +1,55 @@ +package cli + +import ( + "testing" + + "github.com/spf13/cobra" +) + +func TestRenameCLI(t *testing.T) { + cases := []struct{ in, want string }{ + {"flashduty incident list", "fduty incident list"}, + {"run 'flashduty login' first", "run 'fduty login' first"}, + {"Run `flashduty incident timeline `", "Run `fduty incident timeline `"}, + {" flashduty team list\n flashduty team get 1", " fduty team list\n fduty team get 1"}, + {"cat rows.csv | flashduty enrichment upload", "cat rows.csv | fduty enrichment upload"}, + {"flashduty is managed by flashduty-runner", "fduty is managed by flashduty-runner"}, + // Not the command word: product name, paths, domains, identifiers. + {"Flashduty CLI", "Flashduty CLI"}, + {"go-flashduty client", "go-flashduty client"}, + {"~/.flashduty config", "~/.flashduty config"}, + {"skills/flashduty cards", "skills/flashduty cards"}, + {"flashduty.example.com", "flashduty.example.com"}, + {"flashduty", "flashduty"}, + } + for _, c := range cases { + if got := renameCLI(c.in, "fduty"); got != c.want { + t.Errorf("renameCLI(%q) = %q, want %q", c.in, got, c.want) + } + } + // "$" in the name is literal text, not a regexp group reference. + if got, want := renameCLI("run flashduty login", "fd$1"), "run fd$1 login"; got != want { + t.Errorf("renameCLI with $ in name = %q, want %q", got, want) + } +} + +func TestRenameCommandText(t *testing.T) { + root := &cobra.Command{Use: "flashduty", Long: "Run 'flashduty login'."} + child := &cobra.Command{Use: "list", Short: "List", Example: " flashduty team list"} + child.Flags().String("id", "", "from 'flashduty team list'") + root.PersistentFlags().String("x", "", "see 'flashduty login'") + root.AddCommand(child) + + renameCommandText(root, "fduty") + + for _, c := range []struct{ got, want string }{ + {root.Long, "Run 'fduty login'."}, + {child.Example, " fduty team list"}, + {child.Flags().Lookup("id").Usage, "from 'fduty team list'"}, + {root.PersistentFlags().Lookup("x").Usage, "see 'fduty login'"}, + } { + if c.got != c.want { + t.Errorf("got %q, want %q", c.got, c.want) + } + } +} diff --git a/internal/cli/update.go b/internal/cli/update.go index 9597540..16c5151 100644 --- a/internal/cli/update.go +++ b/internal/cli/update.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "runtime" "github.com/spf13/cobra" @@ -54,19 +55,29 @@ func newUpdateCmd() *cobra.Command { } func runInstaller(cmd *cobra.Command) error { + // The installer replaces the binary that is running, under its own name + // and directory, not a default-named copy elsewhere. + binPath, err := os.Executable() + if err == nil { + binPath, err = filepath.EvalSymlinks(binPath) + } + if err != nil { + return fmt.Errorf("locate the running binary: %w", err) + } + name, args := installerCommandSpec(runtime.GOOS, update.InstallShellURL(), update.InstallPowerShellURL()) c := exec.Command(name, args...) c.Stdout = cmd.OutOrStdout() c.Stderr = cmd.ErrOrStderr() c.Stdin = os.Stdin - c.Env = update.InstallerEnv(os.Environ()) + c.Env = update.InstallerEnv(os.Environ(), binPath) if err := c.Run(); err != nil { return fmt.Errorf("update failed: %w", err) } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nUpdate complete. Run 'flashduty version' to verify.\n") + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nUpdate complete. Run '%s version' to verify.\n", cmd.Root().Name()) return nil } diff --git a/internal/cli/version.go b/internal/cli/version.go index e7c72c0..08ad9ff 100644 --- a/internal/cli/version.go +++ b/internal/cli/version.go @@ -45,6 +45,8 @@ func newVersionCmd() *cobra.Command { _, _ = fmt.Fprintln(out, string(b)) return } + // Fixed "flashduty version" prefix whatever the invoked name: tests, + // the issue template and external tooling match on it. _, _ = fmt.Fprintf(out, "flashduty version %s (%s) built %s\n", versionStr, commitStr, dateStr) }, } diff --git a/internal/skilldoc/generate.go b/internal/skilldoc/generate.go index 713772a..20cf8d2 100644 --- a/internal/skilldoc/generate.go +++ b/internal/skilldoc/generate.go @@ -10,7 +10,7 @@ import ( // Fence markers. The generator owns ONLY the text between these; intent→verb // routing, worked examples, and gotchas are hand-written outside the fence. const ( - fenceStartFmt = "" + fenceStartFmt = "" fenceEndFmt = "" ) diff --git a/internal/skilldoc/incident_comment_guidance_test.go b/internal/skilldoc/incident_comment_guidance_test.go index 136cf25..f46db3d 100644 --- a/internal/skilldoc/incident_comment_guidance_test.go +++ b/internal/skilldoc/incident_comment_guidance_test.go @@ -17,7 +17,7 @@ func TestIncidentCardCommentWorkflow(t *testing.T) { body := string(card) quotedHeredoc := "cat > \"$COMMENT_FILE\" <<'FDUTY_COMMENT_7F3A9C2E_EOF'" - commentCommand := `fduty incident comment "$ID" --comment-file "$COMMENT_FILE"` + commentCommand := `flashduty incident comment "$ID" --comment-file "$COMMENT_FILE"` previous := -1 for _, requirement := range []string{quotedHeredoc, commentCommand} { @@ -63,7 +63,7 @@ func TestIncidentCommentFileHeredocPreservesMarkdown(t *testing.T) { } dir := t.TempDir() - script := `fduty() { + script := `flashduty() { # Mimic --comment-file: print the referenced file's bytes verbatim. cat "$5" } @@ -76,7 +76,7 @@ Use ` + "`kubectl get pod`" + ` to inspect the restart. COMMENT_EOF The follow-up is still pending. FDUTY_COMMENT_7F3A9C2E_EOF -fduty incident comment "$ID" --comment-file "$COMMENT_FILE" +flashduty incident comment "$ID" --comment-file "$COMMENT_FILE" ` output, err := exec.Command("bash", "-c", script).CombinedOutput() diff --git a/internal/update/check.go b/internal/update/check.go index ffc71ed..102412c 100644 --- a/internal/update/check.go +++ b/internal/update/check.go @@ -54,15 +54,25 @@ func UpdateBaseURL() string { func InstallShellURL() string { return UpdateBaseURL() + "/install.sh" } func InstallPowerShellURL() string { return UpdateBaseURL() + "/install.ps1" } -func InstallerEnv(base []string) []string { - env := make([]string, 0, len(base)+1) +// InstallerEnv returns base with the installer's MIRROR_URL, +// FLASHDUTY_INSTALL_DIR and INSTALLED_NAME pointed at the update source and at +// binPath, the running binary, so the installer replaces that binary in place +// instead of installing a default-named copy elsewhere. +func InstallerEnv(base []string, binPath string) []string { + env := make([]string, 0, len(base)+3) for _, item := range base { - if strings.HasPrefix(item, "MIRROR_URL=") { + if strings.HasPrefix(item, "MIRROR_URL=") || + strings.HasPrefix(item, "FLASHDUTY_INSTALL_DIR=") || + strings.HasPrefix(item, "INSTALLED_NAME=") { continue } env = append(env, item) } - return append(env, "MIRROR_URL="+UpdateBaseURL()) + return append(env, + "MIRROR_URL="+UpdateBaseURL(), + "FLASHDUTY_INSTALL_DIR="+filepath.Dir(binPath), + "INSTALLED_NAME="+filepath.Base(binPath), + ) } func latestPointerURL() string { diff --git a/internal/update/check_test.go b/internal/update/check_test.go index 51731af..da3fe6b 100644 --- a/internal/update/check_test.go +++ b/internal/update/check_test.go @@ -300,23 +300,31 @@ func TestUpdateBaseURLAndInstallerURLs(t *testing.T) { } } -func TestInstallerEnvPassesUpdateBaseAsMirrorURL(t *testing.T) { +func TestInstallerEnvTargetsUpdateBaseAndRunningBinary(t *testing.T) { t.Setenv("FLASHDUTY_UPDATE_BASE_URL", "https://mirror.example.com/fduty/") t.Setenv("MIRROR_URL", "") - env := InstallerEnv([]string{"PATH=/bin", "MIRROR_URL=https://old.example.com"}) - want := "MIRROR_URL=https://mirror.example.com/fduty" - found := 0 - for _, item := range env { - if strings.HasPrefix(item, "MIRROR_URL=") { - found++ - if item != want { - t.Fatalf("MIRROR_URL entry = %q, want %q", item, want) + binPath := filepath.Join("opt", "tools", "fduty") + env := InstallerEnv([]string{ + "PATH=/bin", + "MIRROR_URL=https://old.example.com", + "FLASHDUTY_INSTALL_DIR=/usr/local/bin", + "INSTALLED_NAME=flashduty", + }, binPath) + for key, want := range map[string]string{ + "MIRROR_URL": "https://mirror.example.com/fduty", + "FLASHDUTY_INSTALL_DIR": filepath.Join("opt", "tools"), + "INSTALLED_NAME": "fduty", + } { + var got []string + for _, item := range env { + if v, ok := strings.CutPrefix(item, key+"="); ok { + got = append(got, v) } } - } - if found != 1 { - t.Fatalf("found %d MIRROR_URL entries, want 1 in %#v", found, env) + if len(got) != 1 || got[0] != want { + t.Errorf("%s entries = %q, want exactly [%q]", key, got, want) + } } } diff --git a/skills/flashduty/SKILL.md b/skills/flashduty/SKILL.md index fcf7daf..5b3c05b 100644 --- a/skills/flashduty/SKILL.md +++ b/skills/flashduty/SKILL.md @@ -1,20 +1,20 @@ --- name: flashduty version: "3.0" -description: "USE FIRST for Flashduty tasks — status pages, incidents, alerts, on-call, monitors, automations, RUM, sourcemaps, members. `fduty` CLI = the whole API. ALWAYS load this skill + read reference/.md for exact verbs & flags BEFORE running fduty. Don't guess or --help-dance." +description: "USE FIRST for Flashduty tasks — status pages, incidents, alerts, on-call, monitors, automations, RUM, sourcemaps, members. `flashduty` CLI = the whole API. ALWAYS load this skill + read reference/.md for exact verbs & flags BEFORE running flashduty. Don't guess or --help-dance." allowed-tools: bash, read hidden: true # internal-only: withheld from skills.sh public discovery (Safari embeds this skill directly). --- # Flashduty CLI -`fduty` is your interface to Flashduty — invoke it from `bash`. This SKILL.md is a **router**: shared model + conventions below, then a domain index. For a real task, **read the one `reference/.md` card first** — it carries every command, flag, enum, and the worked flow for that domain, so you operate without `--help` trial-and-error or guessing command names. +`flashduty` is your interface to Flashduty — invoke it from `bash`. This SKILL.md is a **router**: shared model + conventions below, then a domain index. For a real task, **read the one `reference/.md` card first** — it carries every command, flag, enum, and the worked flow for that domain, so you operate without `--help` trial-and-error or guessing command names. ## Auth & availability - **Auth.** Set your Flashduty app key once — `export FLASHDUTY_APP_KEY=` — or pass `--app-key ` per call. Then just call the verb. - **No curl for the API.** The CLI is the only supported path to Flashduty — never hand-roll an HTTP call. -- **If `fduty: command not found`** (rare — it is normally on PATH at startup): install from the Flashduty CDN into a user-writable dir (no sudo, no hang), then tell the user — don't work around it: `curl -sSL https://static.flashcat.cloud/flashduty-cli/install.sh | FLASHDUTY_INSTALL_DIR="$HOME/.local/bin" INSTALLED_NAME=fduty sh && export PATH="$HOME/.local/bin:$PATH"`. +- **If `flashduty: command not found`** (rare — it is normally on PATH at startup): install from the Flashduty CDN into a user-writable dir (no sudo, no hang), then tell the user — don't work around it: `curl -sSL https://static.flashcat.cloud/flashduty-cli/install.sh | FLASHDUTY_INSTALL_DIR="$HOME/.local/bin" sh && export PATH="$HOME/.local/bin:$PATH"`. - **If `jq` is missing** and you genuinely need JSON filtering, it is fine to install it into a user-writable dir the same way (`$HOME/.local/bin`) and continue. But first ask whether you can avoid `jq` entirely by using `--output-format toon`, `--fields`, a returned `total`, or a server-side aggregation verb (`insight`, `rule-counter-*`, etc.). ## Data model — 3 layers @@ -37,19 +37,19 @@ Append `--output-format toon` to read commands: it drops the per-row repeated ke ## Timestamps — convert before quoting -fduty renders timestamp fields (created_at / updated_at / start_time / …) as **RFC3339 strings in the process's local timezone** in `--json` and `toon` output; unset values are `null` in `--json` (the string `0` in `toon`). In the AI-SRE runner the process timezone is **Etc/UTC** (the env block's `Environment Timezone`), so those strings come out **UTC** — not the user's local time. The env block's `User Timezone` (Asia/Shanghai for zh-CN accounts) is the zone the user reads. +flashduty renders timestamp fields (created_at / updated_at / start_time / …) as **RFC3339 strings in the process's local timezone** in `--json` and `toon` output; unset values are `null` in `--json` (the string `0` in `toon`). In the AI-SRE runner the process timezone is **Etc/UTC** (the env block's `Environment Timezone`), so those strings come out **UTC** — not the user's local time. The env block's `User Timezone` (Asia/Shanghai for zh-CN accounts) is the zone the user reads. -**Rule: before quoting ANY fduty timestamp to the user, convert it to the user's timezone** (Asia/Shanghai for zh-CN accounts; the runner env block states `User Timezone`). Report the converted time and label it 北京时间/本地时间. Never present a raw UTC wall-clock as if it were the user's local time — e.g. a change logged at `2026-08-13T13:05:03Z` happened at **21:05 北京时间**, not 13:05. A timestamp you can't convert must be reported with its original offset and an explicit "(UTC)" tag. +**Rule: before quoting ANY flashduty timestamp to the user, convert it to the user's timezone** (Asia/Shanghai for zh-CN accounts; the runner env block states `User Timezone`). Report the converted time and label it 北京时间/本地时间. Never present a raw UTC wall-clock as if it were the user's local time — e.g. a change logged at `2026-08-13T13:05:03Z` happened at **21:05 北京时间**, not 13:05. A timestamp you can't convert must be reported with its original offset and an explicit "(UTC)" tag. ## Command names — don't guess, read the card -The hot path: **read the domain card** (index below) for the exact verb + flags. Command groups are hyphenated (`status-page`, `alert-event`), not concatenated (`statuspage`) — guessing the wrong form costs a failed call. For a command outside the cards, derive it from its API path: **group = first path segment, verb = the rest joined by `-`** (`POST /status-page/change/create` → `fduty status-page change-create`), then confirm with `fduty --help`. Pass nested-object / array fields as JSON via `--data '{...}'`; typed scalar flags override matching `--data` keys. +The hot path: **read the domain card** (index below) for the exact verb + flags. Command groups are hyphenated (`status-page`, `alert-event`), not concatenated (`statuspage`) — guessing the wrong form costs a failed call. For a command outside the cards, derive it from its API path: **group = first path segment, verb = the rest joined by `-`** (`POST /status-page/change/create` → `flashduty status-page change-create`), then confirm with `flashduty --help`. Pass nested-object / array fields as JSON via `--data '{...}'`; typed scalar flags override matching `--data` keys. **Positional arguments.** A card heading like `### change-create ` means that id can always be passed **positionally**, as the first bare argument (`change-create 5759… --type incident`). On generated/raw-passthrough commands the matching `--` flag (e.g. `--page-id`) is an equally valid alternative — if both are given, the flag wins. A few curated commands (`incident detail`, `automation get`) are positional-only, with no matching flag at all. A heading with no `<…>` takes all inputs as flags. `--help` settles any doubt. -## fduty answers directly — don't grep or browse +## flashduty answers directly — don't grep or browse -Configuration, permission-model, enrichment, monitor, and on-call questions are answered by `fduty` itself (the cards + the live commands). Do **not** grep external documentation or browse the web for something the CLI covers — that usually returns staler information than the live API. Read the card, run the verb. +Configuration, permission-model, enrichment, monitor, and on-call questions are answered by `flashduty` itself (the cards + the live commands). Do **not** grep external documentation or browse the web for something the CLI covers — that usually returns staler information than the live API. Read the card, run the verb. ## Safety — confirm before mutating diff --git a/skills/flashduty/reference/alert.md b/skills/flashduty/reference/alert.md index a00c144..bd808fb 100644 --- a/skills/flashduty/reference/alert.md +++ b/skills/flashduty/reference/alert.md @@ -1,4 +1,4 @@ -# fduty alert — command card +# flashduty alert — command card Prereq: `SKILL.md` read. Read verbs are free. `merge` is **irreversible** (alerts cannot be un-merged). `pipeline-upsert` **replaces** the full pipeline config. Confirm IDs before either. @@ -27,15 +27,15 @@ Prereq: `SKILL.md` read. Read verbs are free. `merge` is **irreversible** (alert ```bash # 1. list contributing alerts (from the incident domain) -fduty incident alerts --output-format toon +flashduty incident alerts --output-format toon # 2. inspect the worst alert -fduty alert get --output-format toon +flashduty alert get --output-format toon # 3. trace raw events deduplicated into that alert -fduty alert events --output-format toon +flashduty alert events --output-format toon # 4. view state transitions (mute/severity changes/operator actions) -fduty alert feed --output-format toon +flashduty alert feed --output-format toon # 5. for a time-window view across alerts, alert-event list is compact by default -fduty alert-event list --channel --since 1h --limit 30 --output-format toon +flashduty alert-event list --channel --since 1h --limit 30 --output-format toon ``` Structured `alert-event list` output stays below 16 KiB: when the requested page would overflow, only the leading rows that fit are emitted — every value intact — and a stderr note says how many of the rows were emitted, so heed it before assuming the page is complete (narrow `--fields` or lower `--limit` to fit more rows per page). A trailing `...` on a value, with a stderr note naming the clipped fields, appears only when one row alone exceeds the budget — heed it before matching on that value, because the clipped text is what a `jq` filter sees. In json/toon mode rows default to the compact projection `event_id,alert_id,event_severity,event_status,event_time,title` (a stderr note says so when it applies); any other response field is one `--fields` away — a key missing from the output means it wasn't selected, not that the server omits it. @@ -44,14 +44,14 @@ Structured `alert-event list` output stays below 16 KiB: when the requested page ```bash # 1. find active critical alerts in the last 4 hours -fduty alert list --severity Critical --active --since 4h --output-format toon +flashduty alert list --severity Critical --active --since 4h --output-format toon # 2. merge (IRREVERSIBLE) — alert IDs are POSITIONAL; --incident-id is a flag # comment text comes from a file, never an inline shell argument (see incident.md's comment workflow) printf '%s' 'Related disk alerts' > /tmp/merge-comment.txt -fduty alert merge --incident-id --comment-file /tmp/merge-comment.txt +flashduty alert merge --incident-id --comment-file /tmp/merge-comment.txt ``` - + ### event-list List events for an alert @@ -194,7 +194,7 @@ as `payments-api / disk_used`. Prefer the `[TPL]` form: it is explicit about where values come from and does not hard-code separators. **Labels the template reads must already exist.** Enrichment runs BEFORE the -pipeline, so labels produced by `fduty enrichment upsert` (extraction, +pipeline, so labels produced by `flashduty enrichment upsert` (extraction, composition, mapping) are available here — but a label produced by a LATER pipeline rule is not, and a typo just renders `` into the title. @@ -202,7 +202,7 @@ pipeline rule is not, and a typo just renders `` into the title. - **All alert verbs are positional except `list` and the two-ID `merge` flag.** Every verb with `` in its `use` form takes that ID as the first bare argument — do NOT pass `--alert-id`. The single exception: `merge` takes the first alert ID positionally AND requires `--incident-id` as a flag (two different IDs, different roles). - **`alert get` vs `alert info`, `alert events` vs `alert-event list`:** both pairs exist; prefer `get`/`events` (shorter, no extra flag); `info`/`event-list` accept `--alert-id` as a flag override for scripting. -- **No server-side title filter on `list`.** To search by title, use `--json` and pipe to `jq`: `fduty alert list --json | jq '.[] | select(.title | test("disk";"i"))'` +- **No server-side title filter on `list`.** To search by title, use `--json` and pipe to `jq`: `flashduty alert list --json | jq '.[] | select(.title | test("disk";"i"))'` - **`list`'s structured output has no `total`/page metadata** — its `--json`/`toon` response is a bare TOP-LEVEL array (see the `list` fence entry above), not a `{items, total}` wrapper. To count matches, project the narrowest field with `--fields` and count elements, or use a wrapper-style verb whose fence shows `total` (e.g. `list-by-ids`). Don't paginate page 1/2/3... just to count alerts — narrow the query instead (`--active`, `--recovered`, `--severity`, `--channel`, `--since`). - **Use `--fields` when hunting IDs, not full rows.** If the task is "find alert IDs / titles / channels / severities", project only those fields first, then drill into one alert with `get` / `events`. Dumping every field for 100 alerts wastes tokens and hides the one row you need. - **`list` time window cap is 31 days**; `--limit` max is 100. For broader queries use `insight` domain. @@ -213,7 +213,7 @@ pipeline rule is not, and a typo just renders `` into the title. ```bash # Find active Critical alerts in a specific channel and view the noisiest one -fduty alert list --severity Critical --active --channel 98765 --since 2h --output-format toon -fduty alert get --output-format toon -fduty alert events --output-format toon +flashduty alert list --severity Critical --active --channel 98765 --since 2h --output-format toon +flashduty alert get --output-format toon +flashduty alert events --output-format toon ``` diff --git a/skills/flashduty/reference/automation.md b/skills/flashduty/reference/automation.md index 747636e..f6f6003 100644 --- a/skills/flashduty/reference/automation.md +++ b/skills/flashduty/reference/automation.md @@ -1,4 +1,4 @@ -# fduty automation - command card +# flashduty automation - command card Prereq: `SKILL.md` read. Automations create AI SRE sessions on a schedule or through an HTTP POST trigger. `create`, `update`, `delete`, and `fire` mutate or start work. If the user directly asks for that action and provides enough detail, treat it as confirmation and do not ask again. @@ -43,7 +43,7 @@ Prereq: `SKILL.md` read. Automations create AI SRE sessions on a schedule or thr ## Hot flow - create from chat ```bash -fduty automation create \ +flashduty automation create \ --name "Daily SRE brief" \ --team-id \ --schedule daily \ @@ -57,7 +57,7 @@ If the user did not specify a team, omit `--team-id` for personal scope. If the ## Hot flow - create an HTTP POST trigger ```bash -fduty automation create \ +flashduty automation create \ --name "Webhook triage" \ --http-post-trigger \ --prompt-file ./automation-prompt.md \ @@ -67,13 +67,13 @@ fduty automation create \ The response can include `http_post_trigger_id`, `http_post_trigger_url`, and one-time `http_post_token`. Tell the user to store the token; it cannot be retrieved later. Rotate it with: ```bash -fduty automation update --rotate-http-post-token --output-format toon +flashduty automation update --rotate-http-post-token --output-format toon ``` ## Hot flow - exact cron ```bash -fduty automation create \ +flashduty automation create \ --name "Weekday 08:05 review" \ --cron-expr "5 8 * * 1-5" \ --prompt "Review open incidents and alert noise before the workday." \ @@ -83,19 +83,19 @@ fduty automation create \ ## Manage and inspect ```bash -fduty automation list --scope all --limit 20 --output-format toon -fduty automation get --output-format toon -fduty automation runs --since 7d --output-format toon +flashduty automation list --scope all --limit 20 --output-format toon +flashduty automation get --output-format toon +flashduty automation runs --since 7d --output-format toon -fduty automation update --disable --output-format toon -fduty automation update --enable --cron-expr "30 1 * * *" --output-format toon -fduty automation delete --force +flashduty automation update --disable --output-format toon +flashduty automation update --enable --cron-expr "30 1 * * *" --output-format toon +flashduty automation delete --force ``` ## Fire an HTTP POST trigger ```bash -fduty automation fire \ +flashduty automation fire \ --token "$FLASHDUTY_AUTOMATION_TRIGGER_TOKEN" \ --text "manual validation run" \ --output-format toon @@ -103,7 +103,7 @@ fduty automation fire \ The trigger API has no idempotency key: retry only when the failed call is known not to have reached the server. Do not invent a token. If it is missing, rotate the trigger token through `update` or ask the user to provide it through their secure shell/environment, not in chat. - + ### create Create an Automation diff --git a/skills/flashduty/reference/calendar.md b/skills/flashduty/reference/calendar.md index 9caee89..ced285b 100644 --- a/skills/flashduty/reference/calendar.md +++ b/skills/flashduty/reference/calendar.md @@ -1,4 +1,4 @@ -# fduty calendar — command card +# flashduty calendar — command card Prereq: `SKILL.md` read. **`delete` is irreversible** (calendar + all its events gone). `event-delete` is irreversible per event. Reads are free; confirm IDs before any delete. @@ -28,21 +28,21 @@ Public holiday calendars (e.g. `zh-cn.china.official`) are read-only — list th ```bash # 1. Find available public-holiday cal IDs for your locale -fduty calendar list --kind region.official.holiday --output-format toon +flashduty calendar list --kind region.official.holiday --output-format toon # 2. Create a personal calendar that inherits CN public holidays, Mon–Fri workdays -fduty calendar create --cal-name "Ops Workdays" \ +flashduty calendar create --cal-name "Ops Workdays" \ --timezone Asia/Shanghai \ --workdays 1,2,3,4,5 \ --extra-cal-ids zh-cn.china.official # → returns cal_id; save it # 3. Mark a make-up workday (補班) on a Saturday -fduty calendar event-upsert --summary "補班 (New Year)" \ +flashduty calendar event-upsert --summary "補班 (New Year)" \ --start-at 2026-01-17 --end-at 2026-01-18 --is-off false # 4. Mark a custom holiday (non-working) -fduty calendar event-upsert --summary "Team offsite" \ +flashduty calendar event-upsert --summary "Team offsite" \ --start-at 2026-03-20 --end-at 2026-03-22 --is-off true ``` @@ -50,13 +50,13 @@ fduty calendar event-upsert --summary "Team offsite" \ ```bash # List all events in January 2026 -fduty calendar event-list --year 2026 --month 1 --output-format toon +flashduty calendar event-list --year 2026 --month 1 --output-format toon # Delete a specific event (get event_id from the list above) -fduty calendar event-delete --cal-id --event-id +flashduty calendar event-delete --cal-id --event-id ``` - + ### create Create calendar @@ -129,7 +129,7 @@ Update calendar ## Gotchas -- **`cal-id` is POSITIONAL on `info`, `update`, `delete`, `event-list`, `event-upsert`** — pass it as the first bare argument: `fduty calendar info `. On `event-delete` both `--cal-id` and `--event-id` are flags (no positional — `use` is bare `event-delete`). +- **`cal-id` is POSITIONAL on `info`, `update`, `delete`, `event-list`, `event-upsert`** — pass it as the first bare argument: `flashduty calendar info `. On `event-delete` both `--cal-id` and `--event-id` are flags (no positional — `use` is bare `event-delete`). - **`event-upsert` creates OR updates** — omit `--event-id` to create; supply it to edit an existing event. The returned `event_id` is what to save for future edits or deletes. - **`list` defaults to `--kind personal`** — you will NOT see public-holiday calendars unless you pass `--kind region.official.holiday`. Add `--no-locale` to see all locales, not just yours. - **`delete` removes the calendar and ALL its events** — confirm `cal_id` with `list` first; irreversible. @@ -140,7 +140,7 @@ Update calendar Mark the Spring Festival week (2026) as non-working in a personal calendar: ```bash -fduty calendar event-upsert cal.abc123 \ +flashduty calendar event-upsert cal.abc123 \ --summary "Spring Festival" \ --start-at 2026-01-28 --end-at 2026-02-04 \ --is-off true \ diff --git a/skills/flashduty/reference/change.md b/skills/flashduty/reference/change.md index cad5043..ee398f7 100644 --- a/skills/flashduty/reference/change.md +++ b/skills/flashduty/reference/change.md @@ -1,4 +1,4 @@ -# fduty change — command card +# flashduty change — command card Prereq: `SKILL.md` read. One read-only verb. Change events are the "what changed" signal you correlate against an incident during fault analysis. @@ -16,13 +16,13 @@ Prereq: `SKILL.md` read. One read-only verb. Change events are the "what changed ```bash # Pull recent changes in the incident's window, then eyeball label/time overlap with the incident. -fduty change list --since 24h --output-format toon +flashduty change list --since 24h --output-format toon # Narrow by the integration or channel that emitted them, or by a keyword: -fduty change list --since 48h --integration --query "deploy" --output-format toon +flashduty change list --since 48h --integration --query "deploy" --output-format toon ``` - + ### list List changes @@ -52,5 +52,5 @@ List changes ```bash # Changes in the last 6h on a specific integration, newest first -fduty change list --since 6h --integration 5759613685214 --output-format toon +flashduty change list --since 6h --integration 5759613685214 --output-format toon ``` diff --git a/skills/flashduty/reference/channel.md b/skills/flashduty/reference/channel.md index 690f106..ca1e7a4 100644 --- a/skills/flashduty/reference/channel.md +++ b/skills/flashduty/reference/channel.md @@ -1,4 +1,4 @@ -# fduty channel — command card +# flashduty channel — command card Prereq: `SKILL.md` read. Read verbs are free; `create`, `update`, `delete`, `disable`, `enable` mutate state — confirm before acting. `delete` is **irreversible**. @@ -27,16 +27,16 @@ Rules INSIDE a channel have their own cards: escalation / 分派策略 → `refe ## Hot flow — create a channel ```bash -# 1. find owning team-id (from `fduty team list --output-format toon`) -fduty channel list --output-format toon +# 1. find owning team-id (from `flashduty team list --output-format toon`) +flashduty channel list --output-format toon # 2. create the channel (no positional; --channel-name and --team-id are required) -fduty channel create --channel-name "production-api" --team-id \ +flashduty channel create --channel-name "production-api" --team-id \ --auto-resolve-timeout 3600 --auto-resolve-mode trigger # → returns channel_id; next, add an escalation rule so incidents page someone: # see reference/escalation.md ``` - + ### create Create channel @@ -114,7 +114,7 @@ Update channel `severity` — or a label written with its `labels.` prefix.** A bare label name is rejected: `equals: [["service"]]` returns `equal service is not supported` (400). This is the most common way a grouping update fails, and it bites - hardest right after `fduty enrichment upsert` creates a new label — the label + hardest right after `flashduty enrichment upsert` creates a new label — the label exists, but it is `labels.` here, never the bare name. Escalation, silence, inhibit and unsubscribe rules live on their own cards: `reference/escalation.md` and `reference/noise.md`. @@ -123,5 +123,5 @@ Escalation, silence, inhibit and unsubscribe rules live on their own cards: `ref - **`channel-id` can be passed positionally or via `--channel-id` — both work** on every verb of this card (`info`, `infos`, `update`, `delete`, `disable`, `enable`). The fence heading `### verb ` shows the shorter positional form; the flag is an equally valid alternative, and if both are given the flag value wins. - **`channel create` requires `--channel-name` and `--team-id`** even though they are not marked `required` in the flag list — the server rejects the request without them. -- **`--plugin-ids` subscribes integrations that already exist; no `fduty` verb creates or configures one.** Creating an integration (email, webhook, an alert source) and reading its inbound address/token live in the console only — they are not part of the open API this CLI speaks, so they are absent from the command tree by design. When the user asks how to *connect* something, answer from the product documentation and point at the console; do not dump and grep the command tree looking for a verb that cannot exist. `channel info` likewise reports the channel's own fields, not integration credentials. +- **`--plugin-ids` subscribes integrations that already exist; no `flashduty` verb creates or configures one.** Creating an integration (email, webhook, an alert source) and reading its inbound address/token live in the console only — they are not part of the open API this CLI speaks, so they are absent from the command tree by design. When the user asks how to *connect* something, answer from the product documentation and point at the console; do not dump and grep the command tree looking for a verb that cannot exist. `channel info` likewise reports the channel's own fields, not integration credentials. - **`delete` on a channel is irreversible** — all rules within it (escalation, silence, inhibit, drop) are also removed. Confirm the `channel-id` against `list` before proceeding. diff --git a/skills/flashduty/reference/enrichment.md b/skills/flashduty/reference/enrichment.md index 01b12de..011baed 100644 --- a/skills/flashduty/reference/enrichment.md +++ b/skills/flashduty/reference/enrichment.md @@ -1,11 +1,11 @@ -# fduty enrichment — command card +# flashduty enrichment — command card Prereq: `SKILL.md` read. Read verbs are free. **`upsert` fully replaces all rules for an integration** (atomic, irreversible in the sense that the previous ruleset is gone); `mapping-schema-delete`, `mapping-api-delete`, `mapping-data-truncate`, and `mapping-data-upload` are irreversible — confirm IDs before running. ## Route here when "告警丰富 / 字段提取 / 标签映射 / 标签组合 / 标签删除 / 映射表 / 映射 API / enrichment rules / label extraction / label composition / mapping schema / lookup table / alert enrichment" → **enrichment**, NOT `route` (routing = which channel an alert goes to) or `template` (notification rendering). You need two kinds of IDs: -- **`integration-id`** (int64) — the integration that produces alerts. Get a real one from **`fduty alert list`** (every alert carries `integration_id` + `integration_name`). It is **NOT** a `channel_id` — `channel list` does not surface integration IDs, so never feed channel IDs here. +- **`integration-id`** (int64) — the integration that produces alerts. Get a real one from **`flashduty alert list`** (every alert carries `integration_id` + `integration_name`). It is **NOT** a `channel_id` — `channel list` does not surface integration IDs, so never feed channel IDs here. - **`schema-id`** / **`api-id`** (MongoDB ObjectID hex string) — from `mapping-schema-list` / `mapping-api-list`. If no specific integration is in scope, ask which one — do **not** enumerate every channel/integration ID and probe each (most 400 `Integration ... not found`; that is not a discovery strategy). @@ -38,7 +38,7 @@ If no specific integration is in scope, ask which one — do **not** enumerate e ```bash # 1. Create schema: define which alert labels are the lookup keys and what labels will be added -fduty enrichment mapping-schema-create \ +flashduty enrichment mapping-schema-create \ --schema-name "service-owner-map" \ --source-labels service \ --result-labels owner_team,oncall_email \ @@ -46,31 +46,31 @@ fduty enrichment mapping-schema-create \ # → returns schema_id (hex string); save it # 2. Populate rows (up to 1000 per call; docs array via --data) -fduty enrichment mapping-data-upsert \ +flashduty enrichment mapping-data-upsert \ --data '{"docs":[{"service":"payments","owner_team":"platform","oncall_email":"platform@example.com"},{"service":"auth","owner_team":"identity","oncall_email":"identity@example.com"}]}' # 3. Verify rows landed -fduty enrichment mapping-data-list --output-format toon +flashduty enrichment mapping-data-list --output-format toon ``` ## Hot flow — attach enrichment rules to an integration ```bash # 1. Find a real integration ID — alerts carry integration_id + integration_name -fduty alert list --limit 20 --output-format toon +flashduty alert list --limit 20 --output-format toon # 2. Check existing rules before replacing -fduty enrichment info --output-format toon +flashduty enrichment info --output-format toon # 3. Upsert rules (full replacement; rules array via --data) -fduty enrichment upsert \ +flashduty enrichment upsert \ --data '{"rules":[{"kind":"mapping","settings":{"mapping_type":"schema","schema_id":"","result_labels":["owner_team","oncall_email"]}},{"kind":"composition","settings":{"result_label":"summary","template":"[{{.owner_team}}] {{.title}}"}}]}' # 4. Confirm the new ruleset -fduty enrichment info --output-format toon +flashduty enrichment info --output-format toon ``` - + ### info Get enrichment rules @@ -205,8 +205,8 @@ Each rule may have an optional `if` AND-filter: `[{"key":"env","oper":"IN","vals ## Gotchas - **`upsert` replaces the entire ruleset atomically.** There is no "add one rule" verb. Always read with `info` first, then reconstruct the full `rules` array before calling `upsert`. Omitting a rule deletes it silently. -- **`integration-id` is POSITIONAL on `info`, `list`, and `upsert`** — pass it as the first bare argument (e.g. `fduty enrichment upsert 12345 --data '...'`), not as `--integration-id`. Similarly, `schema-id` and `api-id` are POSITIONAL on all verbs where the `use` shows `` or ``. -- **`list` accepts multiple integration IDs as positional args** (`use: list [...]`) — pass them space-separated: `fduty enrichment list 101 102 103`. +- **`integration-id` is POSITIONAL on `info`, `list`, and `upsert`** — pass it as the first bare argument (e.g. `flashduty enrichment upsert 12345 --data '...'`), not as `--integration-id`. Similarly, `schema-id` and `api-id` are POSITIONAL on all verbs where the `use` shows `` or ``. +- **`list` accepts multiple integration IDs as positional args** (`use: list [...]`) — pass them space-separated: `flashduty enrichment list 101 102 103`. - **`mapping-data-upsert` requires `docs` via `--data`** — this array cannot be expressed as flat flags. Each doc must include all `source_labels` AND all `result_labels` fields for the schema, or the row is rejected. - **`mapping-schema-create` requires Pro plan** — creating a schema on a free account returns a plan-gate error, not a 404. - **`mapping-data-truncate` wipes all rows immediately** — there is no undo. Use `mapping-data-download` to export a backup CSV first if the data matters. @@ -218,8 +218,8 @@ Each rule may have an optional `if` AND-filter: `[{"key":"env","oper":"IN","vals ```bash # Read current rules for integration 42 (positional), then extend them -fduty enrichment info 42 --output-format toon +flashduty enrichment info 42 --output-format toon # → copy the existing rules[] array, append the new rule, then upsert the full set: -fduty enrichment upsert 42 \ +flashduty enrichment upsert 42 \ --data '{"rules":[,{"kind":"drop","settings":{"drop_labels":["raw_body","_meta"]}}]}' ``` diff --git a/skills/flashduty/reference/escalation.md b/skills/flashduty/reference/escalation.md index ac768a8..ee8dc44 100644 --- a/skills/flashduty/reference/escalation.md +++ b/skills/flashduty/reference/escalation.md @@ -1,4 +1,4 @@ -# fduty channel escalation rules — 分派策略 +# flashduty channel escalation rules — 分派策略 Prereq: `SKILL.md` read. `escalate-rule-list` / `escalate-rule-info` are free reads; `escalate-rule-create/update/delete/enable/disable` mutate who gets @@ -12,8 +12,8 @@ card. Escalation rules live INSIDE a channel (协作空间) and pick the PEOPLE notified once an incident lands there — NOT `reference/route.md` (alert routing picks the *channel*), NOT `reference/schedule.md` (on-call schedules are a notify *target* referenced from layers). Key IDs: **`channel-id` -(int)** from `fduty channel list`; **`rule-id` (MongoDB ObjectID string)** -from `escalate-rule-list`; **`template-id`** from `fduty template list`. +(int)** from `flashduty channel list`; **`rule-id` (MongoDB ObjectID string)** +from `escalate-rule-list`; **`template-id`** from `flashduty template list`. ## Intent → verb @@ -30,15 +30,15 @@ from `escalate-rule-list`; **`template-id`** from `fduty template list`. ```bash # 1. find the channel and its existing rules -fduty channel escalate-rule-list --output-format toon +flashduty channel escalate-rule-list --output-format toon # 2. add the rule (layers is required via --data) -# API field `person_ids` expects member IDs from `fduty member list`. -fduty channel escalate-rule-create \ +# API field `person_ids` expects member IDs from `flashduty member list`. +flashduty channel escalate-rule-create \ --channel-id --rule-name "P1 on-call" --template-id \ --data '{"layers":[{"target":{"person_ids":[],"by":{"critical":["voice","sms"],"warning":["feishu"]}},"notify_step":5,"max_times":3,"escalate_window":30}]}' ``` - + ### escalate-rule-create Create escalation rule @@ -115,9 +115,9 @@ Update escalation rule ## Worked example — inspect a channel's escalation policy ```bash -fduty channel list --name "payments" --output-format toon +flashduty channel list --name "payments" --output-format toon # → find channel_id (e.g. 4201) -fduty channel escalate-rule-list 4201 --output-format toon +flashduty channel escalate-rule-list 4201 --output-format toon # → find rule_id (MongoDB ObjectID string, e.g. "6643abc123def456789012aa") -fduty channel escalate-rule-info --channel-id 4201 --rule-id "6643abc123def456789012aa" --output-format toon +flashduty channel escalate-rule-info --channel-id 4201 --rule-id "6643abc123def456789012aa" --output-format toon ``` diff --git a/skills/flashduty/reference/field.md b/skills/flashduty/reference/field.md index 3cca5d5..306c2e9 100644 --- a/skills/flashduty/reference/field.md +++ b/skills/flashduty/reference/field.md @@ -1,4 +1,4 @@ -# fduty field — command card +# flashduty field — command card Prereq: `SKILL.md` read. Read verbs (`list`, `info`) are free. `delete` is **irreversible** — double-check the field-id before running it. @@ -23,10 +23,10 @@ You need a **`field_id`** (24-char hex ObjectID) — get it from `field list`. ```bash # 1. Check what already exists (avoid duplicate display-name) -fduty field list --output-format toon +flashduty field list --output-format toon # 2. Create a single-select field (field-type + value-type + options are all required here) -fduty field create \ +flashduty field create \ --display-name "Root Cause" \ --field-name "root_cause" \ --field-type single_select \ @@ -35,11 +35,11 @@ fduty field create \ # → returns field_id; save it. # 3. Later: add an option (pass the full replacement list) -fduty field update \ +flashduty field update \ --options "hardware failure" --options "software bug" --options "human error" --options "network issue" ``` - + ### create Create field @@ -91,7 +91,7 @@ Update field ## Gotchas -- **`delete`, `info`, `update` take `` positionally or via `--field-id`** — both work, e.g. `fduty field delete ` or `fduty field delete --field-id `. If both are given, the flag wins. +- **`delete`, `info`, `update` take `` positionally or via `--field-id`** — both work, e.g. `flashduty field delete ` or `flashduty field delete --field-id `. If both are given, the flag wins. - **`--options` replaces the whole list on `update`** — omitting it leaves options unchanged, but a partial list silently drops the missing values. Always pass the full desired set. - **`--field-name` is the machine key** (`[a-zA-Z0-9_]`, starts with letter/underscore, ≤40 chars). It is the stable identifier for downstream enrichment rules — choose it carefully; it cannot be renamed. - **`delete` is permanent and cascades** — any enrichment rules that reference the field by `field_name` will lose their target. Confirm the name against `field list` before deleting. @@ -101,7 +101,7 @@ Update field ```bash # Create a checkbox field (value-type must be bool; options must be omitted) -fduty field create \ +flashduty field create \ --display-name "Needs Postmortem" \ --field-name "needs_postmortem" \ --field-type checkbox \ diff --git a/skills/flashduty/reference/filters.md b/skills/flashduty/reference/filters.md index eea21a4..3bcd9e9 100644 --- a/skills/flashduty/reference/filters.md +++ b/skills/flashduty/reference/filters.md @@ -67,7 +67,7 @@ alert's. Always write the canonical key. ## Building filters from incident labels (scoping a rule to one incident) To scope a rule to one incident's blast radius, build a single AND group from -that incident's own data (`fduty incident detail `): +that incident's own data (`flashduty incident detail `): 1. Start the group with a severity condition: `{"key":"severity","oper":"IN","vals":[""]}`. diff --git a/skills/flashduty/reference/incident.md b/skills/flashduty/reference/incident.md index 4d9a265..b718f11 100644 --- a/skills/flashduty/reference/incident.md +++ b/skills/flashduty/reference/incident.md @@ -1,4 +1,4 @@ -# fduty incident — command card +# flashduty incident — command card Prereq: `SKILL.md` read. Read verbs are free. **Mutating verbs notify responders or alter state** — confirm scope first. `merge` and `remove` are **irreversible**; `remove` permanently deletes. @@ -11,7 +11,7 @@ Prereq: `SKILL.md` read. Read verbs are free. **Mutating verbs notify responders | want | verb | |---|---| | list / search active incidents | `list` | -| CSV export of incidents | `fduty insight incident-export` | +| CSV export of incidents | `flashduty insight incident-export` | | look up by 6-char UI num | `info --num ` | | full detail + AI summary for a 24-char id | `detail ` (narrative) or `info --incident-id ` (same endpoint) | | get structured data for one or more ids | `get [...]` | @@ -47,19 +47,19 @@ Prereq: `SKILL.md` read. Read verbs are free. **Mutating verbs notify responders ```bash # 1. Find unacknowledged critical incidents (last 4h) -fduty incident list --severity Critical --progress Triggered --since 4h --fields incident_id,num,title,incident_severity,progress,start_time,channel_id,detail_url --output-format toon +flashduty incident list --severity Critical --progress Triggered --since 4h --fields incident_id,num,title,incident_severity,progress,start_time,channel_id,detail_url --output-format toon # 2. Get AI summary + full detail (use the 24-char incident_id from step 1) -fduty incident detail --fields incident_id,num,title,incident_severity,progress,ai_summary,root_cause,resolution,alert_cnt,start_time,channel_id,detail_url --output-format toon +flashduty incident detail --fields incident_id,num,title,incident_severity,progress,ai_summary,root_cause,resolution,alert_cnt,start_time,channel_id,detail_url --output-format toon # 3. See contributing alerts -fduty incident alerts +flashduty incident alerts # 4. Check for prior similar incidents (channel-backed only; see Gotchas) -fduty incident similar --limit 5 --output-format toon +flashduty incident similar --limit 5 --output-format toon # 5. Acknowledge ownership -fduty incident ack +flashduty incident ack # 6. Post a status comment — content goes into a file, never a shell argument ID= @@ -69,10 +69,10 @@ cat > "$COMMENT_FILE" <<'FDUTY_COMMENT_7F3A9C2E_EOF' Root cause identified: DB failover. Fix deploying. FDUTY_COMMENT_7F3A9C2E_EOF -fduty incident comment "$ID" --comment-file "$COMMENT_FILE" +flashduty incident comment "$ID" --comment-file "$COMMENT_FILE" # 7. Resolve with root-cause note -fduty incident resolve --root-cause "DB primary failover delay" --resolution "Failover completed; latency normal." +flashduty incident resolve --root-cause "DB primary failover delay" --resolution "Failover completed; latency normal." ``` Projected `similar` lists stay below 16 KiB: when the page would overflow, only the leading rows that fit are emitted — every value intact — and a stderr note says how many rows were emitted. A trailing `...` in a list row, with a stderr note naming the clipped fields, appears only when one row alone exceeds the budget. `detail --fields` is different: it never shortens values — the projection must fit within 8 KiB as requested or the command fails and names the largest fields, so drop some fields (or drop `--fields` for the full unbounded detail) and retry. @@ -95,13 +95,13 @@ If you fetch the pieces by hand instead, run **all seven** — they are cheap re ```bash ID= # 24-char id from `incident list` -fduty incident detail "$ID" --fields incident_id,num,title,incident_severity,progress,ai_summary,root_cause,resolution,alert_cnt,start_time,channel_id,detail_url --output-format toon # ① 详情 + AI summary + alert counts + channel -fduty incident alerts "$ID" # ② contributing alerts (detail's embedded alerts are empty here) -fduty incident timeline "$ID" # ④ timeline (or `incident feed "$ID"` for the paginated view) -fduty incident similar "$ID" --limit 5 --output-format toon # ⑤ similar past incidents (channel-backed; see Gotchas; compact by default) -fduty incident post-mortem-list --channel-ids # ⑥ post-mortems for this incident's channel (verb card: reference/postmortem.md) -fduty change list --since 24h # ③ correlated changes — by shared labels + time; see reference/change.md -fduty incident list --since --until --limit 50 --fields incident_id,num,title,incident_severity,progress,start_time,channel_id --output-format toon # ⑦ concurrent incidents — all channels, any progress, ±15 min around this incident's start_time (from ①) +flashduty incident detail "$ID" --fields incident_id,num,title,incident_severity,progress,ai_summary,root_cause,resolution,alert_cnt,start_time,channel_id,detail_url --output-format toon # ① 详情 + AI summary + alert counts + channel +flashduty incident alerts "$ID" # ② contributing alerts (detail's embedded alerts are empty here) +flashduty incident timeline "$ID" # ④ timeline (or `incident feed "$ID"` for the paginated view) +flashduty incident similar "$ID" --limit 5 --output-format toon # ⑤ similar past incidents (channel-backed; see Gotchas; compact by default) +flashduty incident post-mortem-list --channel-ids # ⑥ post-mortems for this incident's channel (verb card: reference/postmortem.md) +flashduty change list --since 24h # ③ correlated changes — by shared labels + time; see reference/change.md +flashduty incident list --since --until --limit 50 --fields incident_id,num,title,incident_severity,progress,start_time,channel_id --output-format toon # ⑦ concurrent incidents — all channels, any progress, ±15 min around this incident's start_time (from ①) ``` > **Never report a result you didn't fetch.** Do not write "返回空" / "无" / a count for any aspect whose command is **absent from your tool-call history this turn** — write `未查询 — 可运行 ` instead. "Empty" is a claim only a command you actually ran can make; inventing it is the worst failure mode of a fault summary. @@ -112,19 +112,19 @@ fduty incident list --since --until --limit 50 --fields ```bash # Merge two duplicate incidents into a primary (IRREVERSIBLE — confirm first) -fduty incident merge --source , +flashduty incident merge --source , # Record post-incident narrative on the primary -fduty incident reset \ +flashduty incident reset \ --root-cause "Redis OOM on shard-3" \ --impact "Checkout latency P99 >5s for 12 min" \ --resolution "Increased memory limit; deployed hot patch" # Review the event timeline -fduty incident timeline +flashduty incident timeline ``` - + ### ack [...] Acknowledge incident @@ -509,20 +509,20 @@ Update a work item - **`merge` is irreversible**: source incidents are absorbed into target permanently. Always list and confirm both IDs before running. - **`remove --force`** bypasses the interactive confirmation prompt — never pass `--force` unless the user has explicitly said so. - **`assign` needs `--data` for the nested `assigned_to` object** (either `person_ids` or `escalate_rule_id`). Pass member IDs from `member list` in the API field: `--data '{"incident_ids":[""],"assigned_to":{"person_ids":[101]}}'`. `reassign --person ` is simpler for direct member assignment. -- **Responders live in the `responders` array on `detail` / `get` / `list` records — names come resolved, no `member list` join needed.** Canonical extraction: `fduty incident detail --json | jq -r '.responders[] | "\(.person_name) <\(.email)>"'`. On `get` the same array sits one level down because `get` prints a top-level array: `jq -r '.[0].responders[] | ...'`. Each responder object carries `person_id`, `person_name` (server-resolved display name), `email`, `assigned_at`, `acknowledged_at` (`0` until acknowledged), and `as` (role label). Two selection traps: `list` omits `responders` from its default compact projection — add it via `--fields ...,responders`; and `detail --fields` projects only what you name — include `responders` in the list or the key is absent (absent ≠ empty). In plain-text mode both `detail` and single-id `get` already print a `Responders: name1, name2` line. +- **Responders live in the `responders` array on `detail` / `get` / `list` records — names come resolved, no `member list` join needed.** Canonical extraction: `flashduty incident detail --json | jq -r '.responders[] | "\(.person_name) <\(.email)>"'`. On `get` the same array sits one level down because `get` prints a top-level array: `jq -r '.[0].responders[] | ...'`. Each responder object carries `person_id`, `person_name` (server-resolved display name), `email`, `assigned_at`, `acknowledged_at` (`0` until acknowledged), and `as` (role label). Two selection traps: `list` omits `responders` from its default compact projection — add it via `--fields ...,responders`; and `detail --fields` projects only what you name — include `responders` in the list or the key is absent (absent ≠ empty). In plain-text mode both `detail` and single-id `get` already print a `Responders: name1, name2` line. ## Worked example ```bash # Start: a prod alert paged out; you have the 6-char num "A3F9B1" from Slack. # Step 1: resolve the num to full id and get AI summary in one call. -fduty incident info --num A3F9B1 --output-format toon +flashduty incident info --num A3F9B1 --output-format toon # Step 2: acknowledge so teammates see it's being handled. -fduty incident ack +flashduty incident ack # Step 3: after fix, resolve with context. -fduty incident resolve \ +flashduty incident resolve \ --root-cause "Misconfigured health-check threshold after deploy" \ --resolution "Reverted threshold; all pods healthy." ``` diff --git a/skills/flashduty/reference/insight.md b/skills/flashduty/reference/insight.md index d653d83..05417ca 100644 --- a/skills/flashduty/reference/insight.md +++ b/skills/flashduty/reference/insight.md @@ -1,4 +1,4 @@ -# fduty insight — command card +# flashduty insight — command card Prereq: `SKILL.md` read. All `insight` verbs are **read-only** — no mutations, no confirmations needed. @@ -8,7 +8,7 @@ Prereq: `SKILL.md` read. All `insight` verbs are **read-only** — no mutations, "per-person notification counts / 通知次数 / 通知量 (how many notifications did a person receive)" → **insight responder** (`total_notifications`); per-person delivery outcome detail lives in `incident timeline` `i_notify` entries (`person_id` + `failed_reason` only — no answer/接通 field), not here. -Do **not** hand-aggregate from `alert list` / `incident list` — `insight` does server-side aggregation and gives authoritative numbers. Key IDs you may need: `--team-ids` and `--channel-ids` from `fduty channel list` or `fduty team list`; `--responder-ids` from `fduty member list`. +Do **not** hand-aggregate from `alert list` / `incident list` — `insight` does server-side aggregation and gives authoritative numbers. Key IDs you may need: `--team-ids` and `--channel-ids` from `flashduty channel list` or `flashduty team list`; `--responder-ids` from `flashduty member list`. ## Intent → verb @@ -31,20 +31,20 @@ Do **not** hand-aggregate from `alert list` / `incident list` — `insight` does ```bash # account-level roll-up for past 30 days -fduty insight account --start-time 30d --end-time now --output-format toon +flashduty insight account --start-time 30d --end-time now --output-format toon # per-team and per-channel breakdowns (same flags) -fduty insight team --start-time 30d --end-time now --output-format toon -fduty insight channel --start-time 30d --end-time now --output-format toon +flashduty insight team --start-time 30d --end-time now --output-format toon +flashduty insight channel --start-time 30d --end-time now --output-format toon # who responded slowest (per-responder MTTA) -fduty insight responder --start-time 30d --end-time now --output-format toon +flashduty insight responder --start-time 30d --end-time now --output-format toon # top-10 noisiest check sources this week -fduty insight top-alerts --label check --since 7d --output-format toon +flashduty insight top-alerts --label check --since 7d --output-format toon # per-incident list with MTTA/MTTR (uses --since not --start-time) -fduty insight incidents --since 30d --limit 50 --output-format toon +flashduty insight incidents --since 30d --limit 50 --output-format toon ``` ## Hot flow — 月报 CSV export @@ -52,15 +52,15 @@ fduty insight incidents --since 30d --limit 50 --output-format toon ```bash # incident-export takes epoch seconds ONLY (int64, not relative strings) S=$(date -v-30d +%s); E=$(date +%s) -fduty insight incident-export --start-time $S --end-time $E > incidents.csv +flashduty insight incident-export --start-time $S --end-time $E > incidents.csv # responder/channel/team-export accept relative strings -fduty insight responder-export --start-time 30d --end-time now > responders.csv -fduty insight channel-export --start-time 30d --end-time now > channels.csv -fduty insight team-export --start-time 30d --end-time now > teams.csv +flashduty insight responder-export --start-time 30d --end-time now > responders.csv +flashduty insight channel-export --start-time 30d --end-time now > channels.csv +flashduty insight team-export --start-time 30d --end-time now > teams.csv ``` - + ### account Get account-level insight @@ -380,13 +380,13 @@ Both families accept: relative duration (`30d`, `24h`), `now`, `+7d`, a date, or - **`--aggregate-unit`** (on `account`, `alert-topk-by-label`, `channel`, `responder`, `team` and their exports) splits results into time buckets: `day` / `week` / `month`. When set, the window must span ≥24 h; `day` additionally caps the range at 31 days. - **`top-alerts` and `alert-topk-by-label` return the same top-K breakdown** (`label`, `hours`, `total_alert_cnt`, `total_alert_event_cnt`) — `alert-topk-by-label` is the superset (adds `--team-ids`/`--channel-ids`/severity filters and `--start-time`/`--end-time`). Pick one; don't call both for the same question. - **`responder` has no `--limit`/`--page`** — one call returns every responder's rollup for the account. For account-wide load analysis, use that single response; don't cap it and re-fetch for the rest. -- **Iterating on a `jq` filter? Save `--json` once, then re-run `jq` against the saved file.** Each `fduty insight ...` invocation is a real backend query — re-running the whole command per filter tweak multiplies OLAP load for nothing and risks a timeout on a heavy window. +- **Iterating on a `jq` filter? Save `--json` once, then re-run `jq` against the saved file.** Each `flashduty insight ...` invocation is a real backend query — re-running the whole command per filter tweak multiplies OLAP load for nothing and risks a timeout on a heavy window. ## Worked example — identify noisiest check sources ```bash # Top-20 noisiest check sources in the past 7 days, sorted by raw event count -fduty insight alert-topk-by-label \ +flashduty insight alert-topk-by-label \ --label check \ --k 20 \ --orderby total_alert_event_cnt \ diff --git a/skills/flashduty/reference/member.md b/skills/flashduty/reference/member.md index 1d60a1c..e05c229 100644 --- a/skills/flashduty/reference/member.md +++ b/skills/flashduty/reference/member.md @@ -1,10 +1,10 @@ -# fduty member — command card +# flashduty member — command card Prereq: `SKILL.md` read. `invite` sends invitation emails immediately (up to 20 per call). `delete` is **irreversible** — it removes the member from the organization. Default safety check rejects deletes when the member is referenced by escalation rules, schedules, team membership, etc. (pass `--is-force` to bypass). A member provisioned via SSO cannot be deleted at all, even with `--is-force` — disable SSO management for them first. `role-update` **replaces** all role assignments atomically; `role-grant`/`role-revoke` are additive/subtractive. ## Route here when -"成员 / 邀请 / 用户 / 角色 / member / invite / user profile / role assignment / org roster" → **member**. Sibling domains: `team` (team membership lists, not org-level members); `role` (role definitions — get role IDs here first); `person` (resolve a `person_id` → name with `fduty person infos …`, e.g. ids returned by `schedule`/`oncall`/`incident`/`alert` output). Key IDs: **`member_id` (int)** from `member list`; **`role_id` (int)** from `fduty role list`. +"成员 / 邀请 / 用户 / 角色 / member / invite / user profile / role assignment / org roster" → **member**. Sibling domains: `team` (team membership lists, not org-level members); `role` (role definitions — get role IDs here first); `person` (resolve a `person_id` → name with `flashduty person infos …`, e.g. ids returned by `schedule`/`oncall`/`incident`/`alert` output). Key IDs: **`member_id` (int)** from `member list`; **`role_id` (int)** from `flashduty role list`. ## Intent → verb @@ -24,35 +24,35 @@ Prereq: `SKILL.md` read. `invite` sends invitation emails immediately (up to 20 ```bash # 1. find available role IDs -fduty role list --output-format toon +flashduty role list --output-format toon # 2. invite up to 20 members in one call; members array MUST go via --data -fduty member invite \ +flashduty member invite \ --data '{"members":[{"email":"alice@example.com","member_name":"Alice","role_ids":[]},{"email":"bob@example.com","member_name":"Bob","role_ids":[]}]}' # → returns items[].member_id for each new member # 3. confirm they appear (status will be 'pending' until invite accepted) -fduty member list --query "alice" --output-format toon +flashduty member list --query "alice" --output-format toon ``` ## Hot flow — role change for an existing member ```bash # 1. look up the member -fduty member list --query "alice" --output-format toon +flashduty member list --query "alice" --output-format toon # note member_id and current account_role_ids # 2a. add a role without disturbing others (role-id is POSITIONAL) -fduty member role-grant --member-id +flashduty member role-grant --member-id # 2b. OR: set the complete new role list (role-ids positional; replaces ALL roles) -fduty member role-update --member-id +flashduty member role-update --member-id # 3. verify -fduty member list --query "alice" --output-format toon +flashduty member list --query "alice" --output-format toon ``` - + ### delete Delete member @@ -135,12 +135,12 @@ Update member roles ```bash jq -n --rawfile h /tmp/report.html --argjson pids '[,]' \ '{html: $h, subject: "", person_ids: $pids}' \ - | fduty member notify --data - + | flashduty member notify --data - ``` -- **Resolving a `person_id` → name: use `fduty person infos …`, NOT `member list`.** `schedule`/`oncall`/`incident`/`alert` output returns `person_id`s, a **different namespace from `member_id`**. `fduty person infos` (the sibling `person` group) batch-resolves any number of `person_id`s to `person_name` in one call (rows under `.items[]`). Matching `member list` rows on `member_id == ` is wrong, and paginating the full roster to find them silently misses people on later pages. +- **Resolving a `person_id` → name: use `flashduty person infos …`, NOT `member list`.** `schedule`/`oncall`/`incident`/`alert` output returns `person_id`s, a **different namespace from `member_id`**. `flashduty person infos` (the sibling `person` group) batch-resolves any number of `person_id`s to `person_name` in one call (rows under `.items[]`). Matching `member list` rows on `member_id == ` is wrong, and paginating the full roster to find them silently misses people on later pages. - **`invite` members array is body-only — use `--data`.** Individual members cannot be passed as flat flags; the `members` array (with nested `role_ids`, `email`, `phone`, etc.) lives only in the JSON body. Up to 20 members per call. -- **`info-reset ` can be passed positionally or via `--member-id`** — both work: `fduty member info-reset --member-name "New Name"` or `fduty member info-reset --member-id --member-name "New Name"`. If both are given, the flag wins. -- **`role-grant` / `role-revoke` / `role-update` — role IDs can be passed positionally or via `--role-ids`.** Positional is shorter: `fduty member role-grant [...] --member-id `, or pass `--role-ids ,` instead. If both are given, the flag wins. +- **`info-reset ` can be passed positionally or via `--member-id`** — both work: `flashduty member info-reset --member-name "New Name"` or `flashduty member info-reset --member-id --member-name "New Name"`. If both are given, the flag wins. +- **`role-grant` / `role-revoke` / `role-update` — role IDs can be passed positionally or via `--role-ids`.** Positional is shorter: `flashduty member role-grant [...] --member-id `, or pass `--role-ids ,` instead. If both are given, the flag wins. - **`role-update` is a full replacement.** List current roles with `member list` first; omitting a role removes it. - **`delete` default is safe** (checks escalation rules / schedules / team membership). If it rejects with a reference error, review those references before using `--is-force`. An SSO-provisioned member rejects unconditionally — `--is-force` does not override that check. - **Empty `member list` result is authoritative** — if `--query` returns nothing the member does not exist; do not widen the query. @@ -151,16 +151,16 @@ Look up a member then promote them to a new role: ```bash # find member -fduty member list --query "carol" --output-format toon +flashduty member list --query "carol" --output-format toon # → member_id=4217, account_role_ids=[2] # find the admin role ID -fduty role list --output-format toon +flashduty role list --output-format toon # → role_id=1 is "Admin" # grant admin role (keeps existing role 2) -fduty member role-grant 1 --member-id 4217 +flashduty member role-grant 1 --member-id 4217 # confirm -fduty member list --query "carol" --output-format toon +flashduty member list --query "carol" --output-format toon ``` diff --git a/skills/flashduty/reference/monit-datasource.md b/skills/flashduty/reference/monit-datasource.md index 1334dd3..f8def13 100644 --- a/skills/flashduty/reference/monit-datasource.md +++ b/skills/flashduty/reference/monit-datasource.md @@ -1,4 +1,4 @@ -# fduty monit — datasources +# flashduty monit — datasources Prereq: `SKILL.md` + `reference/monit.md` read. Datasources are what every other Flashmonit surface points at: a rule evaluates one, a probe queries one. @@ -6,7 +6,7 @@ Prereq: `SKILL.md` + `reference/monit.md` read. Datasources are what every other "数据源 / 连接数据源 / SLS project / logstore" or "datasource / connect a datasource / SLS discovery" → this card, **when the datasource is something Flashmonit queries** (Prometheus, Loki, VictoriaLogs, SQL engines, SLS). -**"Datasource" is two unrelated things in this product — check which one the user means.** This card is `POST /monit/datasource/*`: the Flashmonit config surface, i.e. the systems Flashmonit *queries*. On-call has its own, older use of the word: the top-level `datasource` group is `POST /datasource/*` and holds IM-integration plumbing (`fduty datasource im-war-room-enabled-list`, `fduty datasource im-person-try-link`), while the On-call **integrations** that *receive* alerts into a channel live in `reference/channel.md`. So 接入告警 / 集成 / 告警来源 → On-call, not here; "连一个 Prometheus / Loki / MySQL 上来查" → here. +**"Datasource" is two unrelated things in this product — check which one the user means.** This card is `POST /monit/datasource/*`: the Flashmonit config surface, i.e. the systems Flashmonit *queries*. On-call has its own, older use of the word: the top-level `datasource` group is `POST /datasource/*` and holds IM-integration plumbing (`flashduty datasource im-war-room-enabled-list`, `flashduty datasource im-person-try-link`), while the On-call **integrations** that *receive* alerts into a channel live in `reference/channel.md`. So 接入告警 / 集成 / 告警来源 → On-call, not here; "连一个 Prometheus / Loki / MySQL 上来查" → here. **Mutating:** `datasource-create`, `datasource-update`, `datasource-delete` — confirm before running. **`datasource-delete` is irreversible**; confirm the target with `datasource-info` first. @@ -33,9 +33,9 @@ Use the selected `id`, never an Agent locator. `enabled=true` is required; `aler Invoke tools through `monit-query` (see `reference/monit-query.md`); the generated `monit datasource-tools-invoke` below is the spec-mirror equivalent entry for the same endpoint. ```bash -fduty monit datasource-list --type redis_node --output-format json \ +flashduty monit datasource-list --type redis_node --output-format json \ | jq '[.[] | {id,name,type_ident,address,edge_cluster_name,enabled,alerting_enabled}]' -fduty monit-query 12345 --tool redis_node.overview --output-format json +flashduty monit-query 12345 --tool redis_node.overview --output-format json ``` One call invokes one named tool. The CLI unwraps HTTP data to `{datasource_id,tool,data,summary?,truncated?}`. The endpoint has no tool catalog: use the datasource-specific skill reference for static names and parameters. Examples include `mysql.lock_contention`, `postgres.activity`, `redis_node.slowlog`, `kafka.consumer_lag`, `elasticsearch.cat`, `prometheus.metric_trends`, `loki.log_patterns`, and `victorialogs.log_patterns`. Do not guess tool parameters. @@ -47,14 +47,14 @@ Tools require all currently routable Edge sessions in the selected cluster to su The same invoke entry also runs query tools named `.query` for ten datasource types: `prometheus`, `mysql`, `postgres`, `oracle`, `clickhouse`, `elasticsearch`, `loki`, `victorialogs`, `sls`, `tencent_cls`. The tool prefix must match the datasource type. `params` is a per-datasource structure: always `expr` plus `execution` (`kind: instant|range|window`, `from_ms`/`to_ms` Unix milliseconds; `range` also needs `max_data_points`). Log types can add `limit`/`direction`; `sls` requires `project`/`logstore`; `tencent_cls` requires `region`/`topic_id`/`syntax`. SQL types take a single read-only statement with `window` execution. ```bash -fduty monit-query 12345 --tool prometheus.query --output-format json --params - <<'FDUTY' +flashduty monit-query 12345 --tool prometheus.query --output-format json --params - <<'FDUTY' {"expr":"sum by (job) (rate(http_requests_total[5m]))","execution":{"kind":"instant","to_ms":1757462400000}} FDUTY ``` Query `data` is the complete Explore result: `format` is `explore_result.v1` and `result.kind` is `samples`, `frames`, or `logs`; log results keep `applied_limit` and `has_more`. Query tools never synthesize `summary` or `truncated`. Query tools require Edge Explore support (protocol v0.68.0); unsupported clusters fail with `edge_upgrade_required`, `mixed_edge_versions`, or `edge_version_unknown` — report as returned, never fall back to another endpoint automatically. - + ### datasource-create Create datasource diff --git a/skills/flashduty/reference/monit-probe.md b/skills/flashduty/reference/monit-probe.md index 6983e69..ea339b7 100644 --- a/skills/flashduty/reference/monit-probe.md +++ b/skills/flashduty/reference/monit-probe.md @@ -1,4 +1,4 @@ -# fduty monit — datasource queries and diagnostics +# flashduty monit — datasource queries and diagnostics Read only the card for the selected task. Use a configured datasource for metrics, logs and database/middleware diagnostics. @@ -12,7 +12,7 @@ Datasource tools use `datasource_id`, one tool per call and static tool guidance Investigations use named datasource tools. Trend/pattern tools take explicit `params.time_range` Unix seconds (up to six hours), while database overview tools observe the current server. Source evidence is not a confirmed root cause. Read warning/truncation fields before interpreting results. - + ### query-data Query structured data diff --git a/skills/flashduty/reference/monit-query.md b/skills/flashduty/reference/monit-query.md index 845b4ef..6855f86 100644 --- a/skills/flashduty/reference/monit-query.md +++ b/skills/flashduty/reference/monit-query.md @@ -1,9 +1,9 @@ -# fduty monit-query — datasource tool invocation +# flashduty monit-query — datasource tool invocation `monit-query` is the unified tool-invocation command for a configured datasource (`POST /monit/datasource/tools/invoke`). One call runs one named tool — there is no separate query vs. diagnostic split. The generated `monit datasource-tools-invoke` is the spec-mirror equivalent entry (see `reference/monit-datasource.md`). ```bash -fduty monit-query --tool '' [--account-id ] [--params ''] +flashduty monit-query --tool '' [--account-id ] [--params ''] ``` - `` — numeric ID from `monit datasource-list` (use the ID, never a name; several configurations may share an address). @@ -14,15 +14,15 @@ Query-tool `params` always carry `expr` plus `execution` (`kind: instant|range|w ```bash # query tool: PromQL instant evaluation -fduty monit-query 12345 --tool prometheus.query --output-format json --params - <<'FDUTY' +flashduty monit-query 12345 --tool prometheus.query --output-format json --params - <<'FDUTY' {"expr":"sum by (job) (rate(http_requests_total[5m]))","execution":{"kind":"instant","to_ms":1757462400000}} FDUTY # diagnostic tool: no params needed -fduty monit-query 12345 --tool redis_node.overview --output-format json +flashduty monit-query 12345 --tool redis_node.overview --output-format json ``` - + ### monit-query Invoke a datasource query or diagnostic tool diff --git a/skills/flashduty/reference/monit-rule.md b/skills/flashduty/reference/monit-rule.md index b50556d..919a9e2 100644 --- a/skills/flashduty/reference/monit-rule.md +++ b/skills/flashduty/reference/monit-rule.md @@ -1,4 +1,4 @@ -# fduty monit — alert rules +# flashduty monit — alert rules Prereq: `SKILL.md` + `reference/monit.md` read. This is the largest Flashmonit surface: rule CRUD, the folder tree, counters, and change history. @@ -27,8 +27,8 @@ Prereq: `SKILL.md` + `reference/monit.md` read. This is the largest Flashmonit s Use `rule-list-basic --folder-id ` only when a real folder ID is available from the user or trusted context; it lists direct rules, not descendants. Add `--include-descendants` (optionally `--query` / `--limit`, capped at 100) to enumerate subtree rules as id/folder_id/name rows. Omitting the ID or passing `0` returns "Folder not found". ```bash -fduty monit rule-list-basic --folder-id --output-format toon -fduty monit rule-list-basic --folder-id --include-descendants --limit 100 --output-format toon +flashduty monit rule-list-basic --folder-id --output-format toon +flashduty monit rule-list-basic --folder-id --include-descendants --limit 100 --output-format toon ``` The public CLI cannot discover the folder tree itself. If folder IDs are unavailable, report that limit rather than guessing IDs or treating a partial rule list as complete. @@ -59,18 +59,18 @@ The public CLI cannot discover the folder tree itself. If folder IDs are unavail ```bash # 1. list direct rules in a folder whose ID is separately known -fduty monit rule-list-basic --folder-id --output-format toon +flashduty monit rule-list-basic --folder-id --output-format toon # 2. enumerate subtree rules (id/folder_id/name only, capped at 100) -fduty monit rule-list-basic --folder-id --include-descendants --limit 100 --output-format toon +flashduty monit rule-list-basic --folder-id --include-descendants --limit 100 --output-format toon # 3. get full config of one rule -fduty monit rule-v2-info --id --output-format toon +flashduty monit rule-v2-info --id --output-format toon # 4. disable several rules at once without touching other fields -fduty monit rule-update-fields --ids , --fields enabled --enabled false +flashduty monit rule-update-fields --ids , --fields enabled --enabled false ``` - + ### rule-audit-detail Get rule audit snapshot diff --git a/skills/flashduty/reference/monit.md b/skills/flashduty/reference/monit.md index 630ec84..86d691a 100644 --- a/skills/flashduty/reference/monit.md +++ b/skills/flashduty/reference/monit.md @@ -1,10 +1,10 @@ -# fduty monit — command card +# flashduty monit — command card Prereq: `SKILL.md` read. Flashmonit is five separate surfaces sharing one command group, so this card is an index: **read the card for the surface you need, not all of them.** ## Route here when -"监控规则 / 告警规则 / 数据源 / PromQL查询 / 日志查询 / 诊断" or "alert rule / datasource / metric query / log pattern / diagnose" → **monit**. NOT `incident` (that domain = the alert graph after rules fire), and **"数据源" here means a system Flashmonit queries** — On-call 集成 / 告警来源 is a different surface (`reference/channel.md`), and the top-level `datasource` group (`fduty datasource im-war-room-enabled-list`) is On-call IM plumbing, not this one. +"监控规则 / 告警规则 / 数据源 / PromQL查询 / 日志查询 / 诊断" or "alert rule / datasource / metric query / log pattern / diagnose" → **monit**. NOT `incident` (that domain = the alert graph after rules fire), and **"数据源" here means a system Flashmonit queries** — On-call 集成 / 告警来源 is a different surface (`reference/channel.md`), and the top-level `datasource` group (`flashduty datasource im-war-room-enabled-list`) is On-call IM plumbing, not this one. ## Which card @@ -18,7 +18,7 @@ Key IDs are shared across all of them: **rule ID (int)** from `rule-list-basic`; Read verbs are free. Mutating verbs change state — confirm before running; each card flags its own, and marks the irreversible ones. - + ### dashboard-create Create dashboard diff --git a/skills/flashduty/reference/noise.md b/skills/flashduty/reference/noise.md index b9858d0..951bff6 100644 --- a/skills/flashduty/reference/noise.md +++ b/skills/flashduty/reference/noise.md @@ -1,4 +1,4 @@ -# fduty channel noise rules — silence / inhibit / drop +# flashduty channel noise rules — silence / inhibit / drop Prereq: `SKILL.md` read. Read verbs (`*-rule-list`) are free; every `silence-rule-*`, `inhibit-rule-*`, `unsubscribe-rule-*` create / update / @@ -9,7 +9,7 @@ enable / disable / delete mutates state — confirm before acting. "静默 / 屏蔽 / 抑制 / 丢弃 / 降噪 / 维护窗口 / silence / mute / inhibit / suppress / drop / discard / noise reduction / maintenance window" → this card. These rules live INSIDE a channel (协作空间): **`channel-id` (int)** -from `fduty channel list` (channel management: `reference/channel.md`); +from `flashduty channel list` (channel management: `reference/channel.md`); **`rule-id` (MongoDB ObjectID string)** from the matching `*-rule-list`. Escalation / 分派策略 → `reference/escalation.md`. @@ -37,22 +37,22 @@ construction rules and the valid key set. ```bash # 1. inspect the incident to silence around — pulls incident_severity + labels -fduty incident detail --output-format toon +flashduty incident detail --output-format toon # 2. channel-id is POSITIONAL on silence-rule-create (see use: "silence-rule-create ") # filters is one AND group: a severity condition plus one labels. condition # per distinguishing label — id-shaped/long/date-shaped/noise-key label values are # dropped, not passed through (construction rules: reference/filters.md). -fduty channel silence-rule-create \ +flashduty channel silence-rule-create \ --rule-name "planned-maintenance-2026-07-01" \ --is-auto-delete \ --data '{"time_filter":{"start_time":1751328000,"end_time":1751371200},"filters":[[{"key":"severity","oper":"IN","vals":["Critical"]},{"key":"labels.service","oper":"IN","vals":["payments-api"]},{"key":"labels.env","oper":"IN","vals":["prod"]}]]}' # 3. verify — read back `filters` to confirm the conditions round-tripped -fduty channel silence-rule-list --output-format toon +flashduty channel silence-rule-list --output-format toon ``` - + ### inhibit-rule-create Create inhibit rule diff --git a/skills/flashduty/reference/postmortem.md b/skills/flashduty/reference/postmortem.md index 868c0a1..ed841eb 100644 --- a/skills/flashduty/reference/postmortem.md +++ b/skills/flashduty/reference/postmortem.md @@ -1,4 +1,4 @@ -# fduty incident post-mortems — command card +# flashduty incident post-mortems — command card Prereq: `SKILL.md` read. `post-mortem-list` / `post-mortem-info` / `post-mortem-template-list` / `post-mortem-template-info` are free reads; @@ -14,7 +14,7 @@ linked to 1–10 incidents; the incidents themselves (triage, resolve, merge) are `reference/incident.md`. Key IDs: **`post-mortem-id` (string)** from `post-mortem-list` (deterministic hash of the linked incident set); **`template-id` (string)** from `post-mortem-template-list`; -**`incident-id` (24-char MongoDB ObjectID)** from `fduty incident list`. +**`incident-id` (24-char MongoDB ObjectID)** from `flashduty incident list`. ## Intent → verb @@ -35,9 +35,9 @@ are `reference/incident.md`. Key IDs: **`post-mortem-id` (string)** from ```bash # 1. pick a template -fduty incident post-mortem-template-list --output-format toon +flashduty incident post-mortem-template-list --output-format toon # 2. initialize the report from the incident (returns post_mortem_id in meta) -fduty incident post-mortem-init --template-id +flashduty incident post-mortem-init --template-id # 3. write the narrative — Markdown goes into a file, then content-reset BODY_FILE=$(mktemp) cat > "$BODY_FILE" <<'FDUTY_PM_7F3A9C2E_EOF' @@ -46,13 +46,13 @@ cat > "$BODY_FILE" <<'FDUTY_PM_7F3A9C2E_EOF' ## Root cause ... FDUTY_PM_7F3A9C2E_EOF -fduty incident post-mortem-content-reset --markdown-file "$BODY_FILE" +flashduty incident post-mortem-content-reset --markdown-file "$BODY_FILE" # 4. follow-ups + publish -fduty incident post-mortem-follow-ups-reset --follow-ups "Add alert on replica lag; tune failover timeout" -fduty incident post-mortem-status-reset --status published +flashduty incident post-mortem-follow-ups-reset --follow-ups "Add alert on replica lag; tune failover timeout" +flashduty incident post-mortem-status-reset --status published ``` - + ### post-mortem-basics-reset Update post-mortem basics @@ -147,7 +147,7 @@ Update post-mortem title ## Gotchas - **Post-mortem verbs live under the `incident` command group** — - `fduty incident post-mortem-list`; there is no standalone post-mortem + `flashduty incident post-mortem-list`; there is no standalone post-mortem group. - **`post-mortem-id` ≠ `incident-id`**: init takes incident IDs and returns the report; every later verb takes the `post-mortem-id` from diff --git a/skills/flashduty/reference/role.md b/skills/flashduty/reference/role.md index 62114e3..3529420 100644 --- a/skills/flashduty/reference/role.md +++ b/skills/flashduty/reference/role.md @@ -1,4 +1,4 @@ -# fduty role — command card +# flashduty role — command card Prereq: `SKILL.md` read. Read verbs are free; `delete` is **irreversible** — confirm the role-id first. `upsert --permission-ids` **replaces** the entire permission set on an existing role. @@ -26,39 +26,39 @@ Prereq: `SKILL.md` read. Read verbs are free; `delete` is **irreversible** — c ```bash # 1. Browse available permissions with role membership annotation -fduty role permission-list --with-all --output-format toon +flashduty role permission-list --with-all --output-format toon # 2. Create the role with chosen permission IDs (note: ids from step 1) -fduty role upsert --role-name "Incident Responder" \ +flashduty role upsert --role-name "Incident Responder" \ --description "Read incidents and manage on-call." \ --permission-ids 101,102,305 # 3. Find the new role ID -fduty role list --output-format toon +flashduty role list --output-format toon # 4. Find member IDs to assign (member-id is POSITIONAL, role-id is a flag) -fduty member list --output-format toon +flashduty member list --output-format toon # 5. Grant role to members (first member-id is positional; additional ids space-separated) -fduty role member-grant --role-id -# Grant to multiple: fduty role member-grant --role-id +flashduty role member-grant --role-id +# Grant to multiple: flashduty role member-grant --role-id ``` ## Hot flow — audit and update an existing role ```bash # 1. Find the role -fduty role list --output-format toon +flashduty role list --output-format toon # 2. Inspect current permissions (is_granted shows which are currently set) -fduty role permission-list --role-ids --with-all --output-format toon +flashduty role permission-list --role-ids --with-all --output-format toon # 3. Update permissions (--permission-ids is the FULL replacement set) -fduty role upsert --role-id --role-name "Incident Responder" \ +flashduty role upsert --role-id --role-name "Incident Responder" \ --permission-ids 101,102,305,410 ``` - + ### delete Delete a role @@ -123,8 +123,8 @@ Create or update a role ## Gotchas -- **`delete`, `disable`, `enable`, `info` take `` positionally or via `--role-id`** — both work: `fduty role delete ` or `fduty role delete --role-id `. If both are given, the flag wins. -- **`member-grant` / `member-revoke`: `` is POSITIONAL (one or more space-separated); `--role-id` is a flag** — easy to flip. Example: `fduty role member-grant 123 456 --role-id 7`. Member IDs can also be passed via `--member-ids` instead of the positional (same fold-then-override rule). +- **`delete`, `disable`, `enable`, `info` take `` positionally or via `--role-id`** — both work: `flashduty role delete ` or `flashduty role delete --role-id `. If both are given, the flag wins. +- **`member-grant` / `member-revoke`: `` is POSITIONAL (one or more space-separated); `--role-id` is a flag** — easy to flip. Example: `flashduty role member-grant 123 456 --role-id 7`. Member IDs can also be passed via `--member-ids` instead of the positional (same fold-then-override rule). - **`upsert --permission-ids` replaces the full set** on update — omitting it clears all permissions. Always read `permission-list --role-ids --with-all` first to get the current set before modifying. - **`upsert` with no `--role-id` (or `--role-id 0`) creates; with `--role-id N` updates** — the verb doubles as create and update; check for an existing role with `list` to avoid accidental duplicates. - **`delete` is irreversible** — members who had this role lose its permissions immediately. Prefer `disable` to park a role without destroying it. @@ -134,7 +134,7 @@ Create or update a role ```bash # Revoke a role from a single member -fduty role member-revoke --role-id +flashduty role member-revoke --role-id # Revoke from multiple members in one call -fduty role member-revoke --role-id +flashduty role member-revoke --role-id ``` diff --git a/skills/flashduty/reference/route.md b/skills/flashduty/reference/route.md index d3cff99..06cf97f 100644 --- a/skills/flashduty/reference/route.md +++ b/skills/flashduty/reference/route.md @@ -1,12 +1,12 @@ -# fduty route — command card +# flashduty route — command card Prereq: `SKILL.md` read. `upsert` is a **full replacement** of the rule — it overwrites all cases; always read first and pass `--version` for optimistic concurrency. ## Route here when "路由规则 / 告警路由 / 集成路由 / 分派到频道 / route rule / alert routing / integration routing / which channel gets alerts" → **route**. Key IDs needed: -- **`integration-id`** (int) — the integration the rule belongs to. Get a real one from **`fduty alert list`** (every alert carries `integration_id` + `integration_name`). It is **NOT** a `channel_id`; `channel list` does not surface integration IDs. If none is in scope, ask which integration rather than probing IDs. -- **`channel-id`** (int) — the target channel matched alerts route to; from `fduty channel list`. +- **`integration-id`** (int) — the integration the rule belongs to. Get a real one from **`flashduty alert list`** (every alert carries `integration_id` + `integration_name`). It is **NOT** a `channel_id`; `channel list` does not surface integration IDs. If none is in scope, ask which integration rather than probing IDs. +- **`channel-id`** (int) — the target channel matched alerts route to; from `flashduty channel list`. Do NOT use `route` for scheduling (→ `schedule`), templates (→ `template`), or channel management (→ `channel`). @@ -22,11 +22,11 @@ Do NOT use `route` for scheduling (→ `schedule`), templates (→ `template`), ```bash # 1. Read the current rule; note the returned `version` field for concurrency control. -fduty route info --output-format toon +flashduty route info --output-format toon # 2. Upsert: route critical alerts to channel 101, all others to channel 102 (default). # Pass the `version` from step 1 to prevent races. -fduty route upsert --version \ +flashduty route upsert --version \ --data '{ "cases": [ { @@ -39,15 +39,15 @@ fduty route upsert --version \ }' # 3. Verify -fduty route info --output-format toon +flashduty route info --output-format toon ``` ```bash # Bulk read — check rules for several integrations at once (positional ids). -fduty route list --output-format toon +flashduty route list --output-format toon ``` - + ### info Get routing rule detail @@ -90,10 +90,10 @@ Upsert routing rule ```bash # Read current rule for integration 5000, then add a name-mapping case for team-based routing. -fduty route info --output-format toon +flashduty route info --output-format toon # → note current version, e.g. 3 -fduty route upsert --version 3 \ +flashduty route upsert --version 3 \ --data '{ "cases": [ { diff --git a/skills/flashduty/reference/rum.md b/skills/flashduty/reference/rum.md index e872ead..98136ba 100644 --- a/skills/flashduty/reference/rum.md +++ b/skills/flashduty/reference/rum.md @@ -1,4 +1,4 @@ -# fduty rum — command card +# flashduty rum — command card Prereq: `SKILL.md` read. Read verbs are free. `application-create` / `application-update` / `application-delete` / `issue-update` mutate state — confirm before running. `application-delete` is **irreversible**. @@ -31,35 +31,35 @@ Prereq: `SKILL.md` read. Read verbs are free. `application-create` / `applicatio ```bash # 1. find the app (application_id is a string) -fduty rum application-list --query "checkout" --output-format toon +flashduty rum application-list --query "checkout" --output-format toon # 2. list open errors in the last 7 days (both time flags required, MILLISECOND epoch) NOW=$(date +%s000) WEEK_AGO=$(( $(date +%s) - 604800 ))000 -fduty rum issue-list \ +flashduty rum issue-list \ --application-ids \ --start-time $WEEK_AGO --end-time $NOW \ --statuses for_review --orderby error_count \ --output-format toon # 3. get full detail of the top issue -fduty rum issue-info --output-format toon +flashduty rum issue-info --output-format toon # 4. mark resolved after fix is confirmed -fduty rum issue-update --status resolved --suspected-cause code.exception +flashduty rum issue-update --status resolved --suspected-cause code.exception ``` ## Hot flow — create a new RUM application ```bash # team-id is POSITIONAL (use: "application-create "); other fields are flags -fduty rum application-create \ +flashduty rum application-create \ --application-name "Checkout Web" \ --type browser # → returns application_id + client_token for SDK init ``` - + ### application-create Create application @@ -386,20 +386,20 @@ Regression: a `resolved` issue that recurs gets a `regression{}` object on its r - **`alerting` and `tracing` are nested objects** — configure them via `--data '{"alerting":{...},"tracing":{...}}'`; there are no flat flags for their sub-fields. Scalar flags (`--application-name`, `--type`, …) override matching `--data` keys. - **Application records hold CONFIG only** — no traffic volume, error-rate, or session-count fields. For trend data, query `monit` RUM series. - **Empty `issue-list` is authoritative** — a filter returning no items means no matching issues, not a missing feature. Do not widen the query or guess. -- **No `rum sourcemap` subcommand** — sourcemap lookup and stack enrichment are top-level: read `reference/sourcemap.md` and use `fduty sourcemap ...`. +- **No `rum sourcemap` subcommand** — sourcemap lookup and stack enrichment are top-level: read `reference/sourcemap.md` and use `flashduty sourcemap ...`. ## Worked example ```bash # Find the worst unreviewed crash in the "payment" app this week, then mark it resolved -APP_ID=$(fduty rum application-list --query "payment" --output-format json | jq -r '.items[0].application_id') +APP_ID=$(flashduty rum application-list --query "payment" --output-format json | jq -r '.items[0].application_id') NOW=$(date +%s000) WEEK_AGO=$(( $(date +%s) - 604800 ))000 -fduty rum issue-list \ +flashduty rum issue-list \ --application-ids "$APP_ID" \ --start-time $WEEK_AGO --end-time $NOW \ --statuses for_review --orderby session_count \ --limit 1 --output-format json | jq -r '.items[0].issue_id' # → paste the returned issue_id below -fduty rum issue-update --status resolved --suspected-cause code.exception +flashduty rum issue-update --status resolved --suspected-cause code.exception ``` diff --git a/skills/flashduty/reference/safari.md b/skills/flashduty/reference/safari.md index 664ffb9..9c29279 100644 --- a/skills/flashduty/reference/safari.md +++ b/skills/flashduty/reference/safari.md @@ -1,8 +1,8 @@ -# fduty safari — command card +# flashduty safari — command card Prereq: `SKILL.md` read. This is the **AI-SRE platform self-management** group: install/configure the account's own **MCP servers (connectors)**, **skills**, and **A2A agents**, plus inspect **sessions**. Mutating verbs (`create`, `update`, `delete`, `upload`) change account configuration — confirm before running. `delete` is **irreversible**. -> Registering an MCP server is THIS group (`fduty safari mcp-server-create`) — **not** a tool search. A tool search only discovers callable tools on servers already connected to you; it can neither register nor configure one. +> Registering an MCP server is THIS group (`flashduty safari mcp-server-create`) — **not** a tool search. A tool search only discovers callable tools on servers already connected to you; it can neither register nor configure one. ## Route here when @@ -32,17 +32,17 @@ Pass the nested `env` / `headers` objects through `--data` (they have no scalar ```bash # stdio (local process): command + args + secrets via env -fduty safari mcp-server-create --data '{"server_name":"GitHub Tools","transport":"stdio","description":"Read issues and pull requests from GitHub.","command":"npx","args":["-y","@modelcontextprotocol/server-github"],"env":{"GITHUB_TOKEN":"ghp_xxx"},"team_id":0,"status":"enabled"}' +flashduty safari mcp-server-create --data '{"server_name":"GitHub Tools","transport":"stdio","description":"Read issues and pull requests from GitHub.","command":"npx","args":["-y","@modelcontextprotocol/server-github"],"env":{"GITHUB_TOKEN":"ghp_xxx"},"team_id":0,"status":"enabled"}' # remote (streamable-http) with per-user OAuth — oauth_metadata stays empty (auto-discovered + DCR at runtime) -fduty safari mcp-server-create --data '{"server_name":"Aliyun OpenAPI","transport":"streamable-http","description":"Alibaba Cloud OpenAPI MCP.","url":"https://openapi-mcp.example.com/mcp","auth_mode":"per_user_oauth","team_id":0,"status":"enabled"}' +flashduty safari mcp-server-create --data '{"server_name":"Aliyun OpenAPI","transport":"streamable-http","description":"Alibaba Cloud OpenAPI MCP.","url":"https://openapi-mcp.example.com/mcp","auth_mode":"per_user_oauth","team_id":0,"status":"enabled"}' # confirm it registered, then inspect its live tool catalogue -fduty safari mcp-server-list --output-format toon -fduty safari mcp-server-get --data '{"server_id":"mcp_xxx"}' +flashduty safari mcp-server-list --output-format toon +flashduty safari mcp-server-get --data '{"server_id":"mcp_xxx"}' ``` - + ### a2a-agent-create Create A2A agent diff --git a/skills/flashduty/reference/schedule.md b/skills/flashduty/reference/schedule.md index 1bbef4f..f30fa80 100644 --- a/skills/flashduty/reference/schedule.md +++ b/skills/flashduty/reference/schedule.md @@ -1,4 +1,4 @@ -# fduty schedule — command card +# flashduty schedule — command card Prereq: `SKILL.md` read. **Read verbs are free. `delete` is irreversible — confirm IDs before executing. `create` / `update` immediately change the live rotation — confirm scope first.** @@ -27,37 +27,37 @@ Prereq: `SKILL.md` read. **Read verbs are free. `delete` is irreversible — con ```bash # 1. Find the schedule ID(s) — scope by name or team; don't fetch every schedule -fduty schedule list --query "SRE" --output-format toon -# or, for several: fduty schedule list --team-ids --output-format toon +flashduty schedule list --query "SRE" --output-format toon +# or, for several: flashduty schedule list --team-ids --output-format toon # 2. Current on-call for each schedule — a tiny now-window yields the live shift -fduty schedule info --start now --end +1h --output-format toon +flashduty schedule info --start now --end +1h --output-format toon ``` `--output-format toon` is for reading, not piping — it is **not** jq-parseable. If you need to filter/extract with `jq`, use `--json` instead. -`schedule info`'s on-call groups carry `person_ids` (integers), not names. Resolve every id in **one batch call** with `fduty person infos` (the sibling `person` group — takes positional ids or `--person-ids`): +`schedule info`'s on-call groups carry `person_ids` (integers), not names. Resolve every id in **one batch call** with `flashduty person infos` (the sibling `person` group — takes positional ids or `--person-ids`): ```bash # person_ids come straight from the schedule/oncall output above -fduty person infos [ …] --output-format toon +flashduty person infos [ …] --output-format toon # → rows under .items[] with person_id + person_name; join on person_id client-side ``` -**`person_id` ≠ `member_id` — do NOT resolve schedule/oncall people via `member list`.** They are different id namespaces, so matching `member list` rows on `member_id == ` is wrong, and paginating the full roster (often 20+ pages) silently drops people who land on later pages — a real prod miss. Always feed the `person_id`s to `fduty person infos`. If a lookup genuinely fails, report the bare `person_id` rather than guessing. +**`person_id` ≠ `member_id` — do NOT resolve schedule/oncall people via `member list`.** They are different id namespaces, so matching `member list` rows on `member_id == ` is wrong, and paginating the full roster (often 20+ pages) silently drops people who land on later pages — a real prod miss. Always feed the `person_id`s to `flashduty person infos`. If a lookup genuinely fails, report the bare `person_id` rather than guessing. ## Hot flow — inspect a schedule's upcoming shifts ```bash -fduty schedule list --query "SRE" --output-format toon # find the schedule_id -fduty schedule info --start now --end +7d --output-format toon # next 7 days +flashduty schedule list --query "SRE" --output-format toon # find the schedule_id +flashduty schedule info --start now --end +7d --output-format toon # next 7 days ``` ## Hot flow — create a schedule via --data ```bash # Layers are deeply nested; pass the full body via --data; scalar flags override matching keys. -fduty schedule create --schedule-name "SRE Weekly" --team-id \ +flashduty schedule create --schedule-name "SRE Weekly" --team-id \ --data '{ "layers": [{ "layer_name": "Week rotation", @@ -93,10 +93,10 @@ fduty schedule create --schedule-name "SRE Weekly" --team-id \ "update_by": 0 }] }' -# → returns schedule_id; verify with: fduty schedule info --start now --end +7d +# → returns schedule_id; verify with: flashduty schedule info --start now --end +7d ``` - + ### by-person Get member on-call status @@ -189,7 +189,7 @@ Update schedule ## Gotchas -- **`info` takes `` positionally or via `--schedule-id`** — both work: `fduty schedule info 123 --start now --end +7d` or `fduty schedule info --schedule-id 123 --start now --end +7d`. If both are given, the flag wins. **`infos` and `delete` have no singular `--schedule-id` flag** — only the plural `--schedule-ids` (comma-separated), which folds the same way as the positional list; passing `--schedule-id` there is an unknown-flag error, not a rejected alternative. +- **`info` takes `` positionally or via `--schedule-id`** — both work: `flashduty schedule info 123 --start now --end +7d` or `flashduty schedule info --schedule-id 123 --start now --end +7d`. If both are given, the flag wins. **`infos` and `delete` have no singular `--schedule-id` flag** — only the plural `--schedule-ids` (comma-separated), which folds the same way as the positional list; passing `--schedule-id` there is an unknown-flag error, not a rejected alternative. - **`create` / `update` / `preview` take all inputs as flags** (no positional). `update` requires `--schedule-id` as a flag to identify the target. - **`layers` is body-only.** There is no per-layer typed flag — you must pass the entire `layers` array via `--data`. Scalar top-level flags (`--schedule-name`, `--team-id`) override matching `--data` keys. - **`list` without `--start`/`--end` omits computed shifts** — only schedule metadata is returned. Pass both flags (≤45 day span) to get rotation slots in the list response. @@ -201,5 +201,5 @@ Update schedule ```bash # Find my own on-call windows for the next two weeks -fduty schedule self --start now --end +14d --output-format toon +flashduty schedule self --start now --end +14d --output-format toon ``` diff --git a/skills/flashduty/reference/sourcemap.md b/skills/flashduty/reference/sourcemap.md index 23f3e27..540128a 100644 --- a/skills/flashduty/reference/sourcemap.md +++ b/skills/flashduty/reference/sourcemap.md @@ -1,4 +1,4 @@ -# fduty sourcemap — command card +# flashduty sourcemap — command card Prereq: `SKILL.md` read. These verbs are read-only/debugging helpers. They do not upload, delete, or mutate sourcemap records. @@ -17,7 +17,7 @@ Prereq: `SKILL.md` read. These verbs are read-only/debugging helpers. They do no ```bash # 1. Confirm the service/version/type actually has uploaded mapping files -fduty sourcemap list \ +flashduty sourcemap list \ --type browser \ --services checkout-web \ --start-time 1712000000000 \ @@ -25,12 +25,12 @@ fduty sourcemap list \ --output-format toon # 2. Enrich the minified stack trace. Use --data for multiline stack payloads. -fduty sourcemap stack-enrich \ +flashduty sourcemap stack-enrich \ --data '{"type":"browser","service":"checkout-web","version":"1.0.0","near":3,"stack":"TypeError: Cannot read properties of undefined\n at render (https://cdn.example.com/app.min.js:1:2345)"}' \ --output-format toon ``` - + ### list List sourcemaps @@ -70,7 +70,7 @@ Enrich a stack trace ## Gotchas -- **Top-level group:** use `fduty sourcemap ...`, not `fduty rum sourcemap ...`. +- **Top-level group:** use `flashduty sourcemap ...`, not `flashduty rum sourcemap ...`. - **`stack-enrich` needs exact upload identity:** `type`, `service`, and `version` must match the uploaded sourcemap/dSYM metadata. - **Use `--data` for stack traces.** Multiline stacks are easier and safer as JSON body payloads than shell-escaped flags. - **Empty `list` is authoritative** for the supplied filters; re-check service/version/type from the RUM app or build metadata before changing the time window. diff --git a/skills/flashduty/reference/status-page.md b/skills/flashduty/reference/status-page.md index aba1f1d..7f4150c 100644 --- a/skills/flashduty/reference/status-page.md +++ b/skills/flashduty/reference/status-page.md @@ -1,4 +1,4 @@ -# fduty status-page — command card +# flashduty status-page — command card Prereq: `SKILL.md` read. **SKILL.md + this card = full competence on status pages — no `--help` needed.** Read verbs are free; any `change-*` create/update with `--notify-subscribers` pages subscribers immediately — confirm scope first. @@ -29,25 +29,25 @@ Structure mutations (`component-upsert` / `component-delete` / `section-upsert` ```bash # page-id is POSITIONAL here (see fence headings: `### change-active-list `); change-id stays a flag. # 1. find the page + impacted component IDs -fduty status-page list --output-format toon +flashduty status-page list --output-format toon # 2. confirm nothing already open (empty = nothing open; if one exists, reuse its change_id) -fduty status-page change-active-list --type incident +flashduty status-page change-active-list --type incident # 3. open it (page-id positional; scalars as flags; the required `updates` array via --data); save change_id -fduty status-page change-create --type incident \ +flashduty status-page change-create --type incident \ --title "API latency elevated" --status investigating --description "Investigating elevated latency." \ --data '{"updates":[{"status":"investigating","description":"Team is investigating.","component_changes":[{"component_id":"","status":"degraded"}]}]}' # 4. post progress: investigating → identified → monitoring (change-timeline-create takes BOTH ids as flags) -fduty status-page change-timeline-create --page-id --change-id \ +flashduty status-page change-timeline-create --page-id --change-id \ --status identified --description "Root cause identified." # 5. resolve — every referenced component MUST go back to operational -fduty status-page change-timeline-create --page-id --change-id \ +flashduty status-page change-timeline-create --page-id --change-id \ --status resolved --description "Recovered." \ --data '{"component_changes":[{"component_id":"","status":"operational"}]}' # 6. confirm closed -fduty status-page change-active-list --type incident +flashduty status-page change-active-list --type incident ``` - + ### change-active-list List active status page events @@ -284,7 +284,7 @@ Update status page ## Worked example — open an incident ```bash -fduty status-page change-create --type incident \ +flashduty status-page change-create --type incident \ --title "Web Console Degraded" --status investigating \ --description "Investigating degraded performance on the web console." \ --data '{"updates":[{"status":"investigating","description":"Team is investigating.","component_changes":[{"component_id":"","status":"degraded"}]}]}' diff --git a/skills/flashduty/reference/team.md b/skills/flashduty/reference/team.md index 2f891fa..7ea3bc5 100644 --- a/skills/flashduty/reference/team.md +++ b/skills/flashduty/reference/team.md @@ -1,12 +1,12 @@ -# fduty team — command card +# flashduty team — command card Prereq: `SKILL.md` read. **SKILL.md + this card = full competence on teams — no `--help` needed.** Read verbs are free; `delete` is **irreversible** (always `--force` in scripted contexts); `update --person-ids` **replaces** the entire member list — dangerous without a prior `get`. ## Route here when "团队 / 成员管理 / 创建团队 / 查找团队 / HR同步 / team ID / person ID归属" → **team**. Key IDs: -- **`team_id` (int64)** — from `fduty team list` or `team get --name`. -- **`--person-ids` inputs are member IDs** — look up via `fduty member list --query ` (member card, not here). The API field is named `person_ids`, but team membership expects member IDs. +- **`team_id` (int64)** — from `flashduty team list` or `team get --name`. +- **`--person-ids` inputs are member IDs** — look up via `flashduty member list --query ` (member card, not here). The API field is named `person_ids`, but team membership expects member IDs. NOT this card: on-call schedules (oncall), incidents (incident), channels (channel). @@ -27,24 +27,24 @@ NOT this card: on-call schedules (oncall), incidents (incident), channels (chann ```bash # 1. Check name doesn't already exist -fduty team list --name "SRE Platform" --output-format toon +flashduty team list --name "SRE Platform" --output-format toon # 2. Create with initial members (member IDs from member list) -fduty team create --name "SRE Platform" --description "Site Reliability" \ +flashduty team create --name "SRE Platform" --description "Site Reliability" \ --person-ids 1001,1002,1003 # 3. Verify — note the returned team_id -fduty team get --name "SRE Platform" --output-format toon +flashduty team get --name "SRE Platform" --output-format toon ``` ## Hot flow — update members safely ```bash # ALWAYS read current members before --person-ids (it REPLACES, not appends) -fduty team get --id --output-format toon +flashduty team get --id --output-format toon # Then pass the FULL desired set (existing + new) -fduty team update --id --person-ids 1001,1002,1003,1004 +flashduty team update --id --person-ids 1001,1002,1003,1004 ``` - + ### create Create a new team @@ -125,14 +125,14 @@ Create or update a team - **`--person-ids` on `update` / `create` / `upsert` is a full replacement**, not an append. Read the current list with `get --id` first, or you will silently remove members. - **`get` vs `info`** — both fetch a single team; `get` accepts `--id`/`--name`/`--ref-id`; `get []` also allows the ID as a positional arg. `info` uses `--team-id`/`--team-name`/`--ref-id` flags only. Prefer `get` for interactive lookup. - **`delete` is irreversible** and requires confirmation unless `--force` is set. Always confirm the correct `--id` (not `--name`) in scripts to avoid name-collision accidents. -- **`infos` accepts IDs either way** — space-separated positional args or comma-separated `--team-ids`: `fduty team infos 101 102 103` or `fduty team infos --team-ids 101,102,103`. If both are given, the flag wins. +- **`infos` accepts IDs either way** — space-separated positional args or comma-separated `--team-ids`: `flashduty team infos 101 102 103` or `flashduty team infos --team-ids 101,102,103`. If both are given, the flag wins. - **`upsert` requires `--team-name`** even when updating by `--team-id`; omitting it returns a validation error. ## Worked example ```bash # Idempotent HR-sync upsert: create "Payments" or reset its membership if it already exists -fduty team upsert --team-name "Payments" \ +flashduty team upsert --team-name "Payments" \ --description "Payments engineering" \ --person-ids 2001,2002,2003 \ --reset-if-name-exist \ diff --git a/skills/flashduty/reference/template.md b/skills/flashduty/reference/template.md index 41e440e..5f01e07 100644 --- a/skills/flashduty/reference/template.md +++ b/skills/flashduty/reference/template.md @@ -1,4 +1,4 @@ -# fduty template — command card +# flashduty template — command card Prereq: `SKILL.md` read. Read verbs are free. `create`, `update`, `delete` mutate account-wide notification templates — confirm before running. `delete ` is **irreversible**. @@ -29,25 +29,25 @@ channel — but read the clearing caveat under Gotchas before you try to empty o ```bash # 1. Fetch the built-in preset as a starting point (channel enum below) -fduty template get-preset --channel feishu --output-format toon +flashduty template get-preset --channel feishu --output-format toon # 2. Save the source, edit in an editor, then validate from file -fduty template validate --channel feishu --file ./feishu.tpl +flashduty template validate --channel feishu --file ./feishu.tpl # 3. Preview with a real incident for realistic rendering (no file — inline content) -fduty template preview \ +flashduty template preview \ --type feishu \ --content "$(cat ./feishu.tpl)" \ --incident-id # 4. Create the template (template-name unique per account) -fduty template create \ +flashduty template create \ --template-name "Critical-Feishu-v2" \ --feishu "$(cat ./feishu.tpl)" \ --team-id 0 # 5. Verify -fduty template info --output-format toon +flashduty template info --output-format toon ``` ## Hot flow — change one channel on an existing template @@ -59,25 +59,25 @@ fduty template info --output-format toon T= # POSITIONAL on update/info/delete; --template-name always required # 1. Pull the current source of the channel you are changing -fduty template info "$T" --json > /tmp/tpl.json +flashduty template info "$T" --json > /tmp/tpl.json jq -r '.feishu_app' /tmp/tpl.json > /tmp/feishu_app.tpl # …edit /tmp/feishu_app.tpl… # 2. Preview the edited source against a REAL incident before writing jq -n --rawfile c /tmp/feishu_app.tpl \ '{type:"feishu_app", content:$c, incident_id:""}' \ - | fduty template preview --data - + | flashduty template preview --data - # 3. Write that one channel. NEVER move the body through "$(...)": command substitution # strips every trailing newline, so a body ending in a blank line is silently shortened. jq -n --rawfile feishu_app /tmp/feishu_app.tpl \ --arg t "$T" --arg n "" \ '{template_id:$t, template_name:$n, feishu_app:$feishu_app}' \ - | fduty template update --data - + | flashduty template update --data - # 4. Verify the body round-tripped byte-for-byte — cmp catches a silent truncation that # "the field is still non-empty" would not. -fduty template info "$T" --json | jq -r '.feishu_app' | cmp - /tmp/feishu_app.tpl \ +flashduty template info "$T" --json | jq -r '.feishu_app' | cmp - /tmp/feishu_app.tpl \ && echo "round-trip OK" ``` @@ -85,7 +85,7 @@ Every channel you did not name is untouched — that is the server contract now, Verify the body you wrote anyway: `cmp` is what separates "wrote the right bytes" from "wrote something non-empty". - + ### create Create a template @@ -219,8 +219,8 @@ Note: `create` / `update` flags use **hyphenated** names (`--dingtalk-app`, `--f it.) `--template-name` is still required on every update even when unchanged. - **To CLEAR a channel you must send it as an explicit empty string** — omitting it now means "keep", not "clear". `--dingtalk-app ''` is the intent, but a flag set to the empty - string was dropped before it reached the wire in `fduty` **older than v1.4.2**, which - makes clearing a silent no-op on those builds. Check `fduty --version` first; if it is + string was dropped before it reached the wire in `flashduty` **older than v1.4.2**, which + makes clearing a silent no-op on those builds. Check `flashduty --version` first; if it is older, clear via `--data` with the field spelled out — `--data '{"template_id":"…", "template_name":"…","dingtalk_app":""}'` — and confirm with `info --json` that the channel actually went empty. @@ -232,7 +232,7 @@ Note: `create` / `update` flags use **hyphenated** names (`--dingtalk-app`, `--f - **`--feishu-app-card-v2-table-enabled` uses pointer semantics on `update`** — unlike the plain string channel-content flags, it patches the table-rendering setting only when the flag is explicitly passed; omit it to leave the existing setting untouched. It is a plain bool on `create` (no prior setting to preserve). - **`list` returns every channel's full template source for every row** — a few dozen templates blow past a tool-output cap in one call. Never render it directly: go to a - file and project. `fduty template list --limit 100 --json > /tmp/tpl_list.json && jq -r + file and project. `flashduty template list --limit 100 --json > /tmp/tpl_list.json && jq -r '.items[] | [.template_id, .template_name, .team_id] | @tsv' /tmp/tpl_list.json`. - **`delete` is permanent.** The built-in preset (`template_id = 000000000000000000000001`) can be addressed by that sentinel ID in `info` and `delete` — don't delete it. - **`validate` reads from a local `--file`; `preview` takes inline `--content`.** They are complementary: `validate` gives size-vs-limit diagnostics; `preview` renders against real or mock incident data. @@ -243,9 +243,9 @@ Note: `create` / `update` flags use **hyphenated** names (`--dingtalk-app`, `--f ```bash # Browse variables available in templates, then validate a draft -fduty template variables --category core --output-format toon -fduty template validate --channel slack --file ./slack-draft.tpl --incident +flashduty template variables --category core --output-format toon +flashduty template validate --channel slack --file ./slack-draft.tpl --incident # On success, create it -fduty template create --template-name "Ops-Slack-Alert" --slack "$(cat ./slack-draft.tpl)" +flashduty template create --template-name "Ops-Slack-Alert" --slack "$(cat ./slack-draft.tpl)" # → returns template_id; assign it to a channel in the escalation policy UI. ``` diff --git a/skills/flashduty/scripts/incident-summary.sh b/skills/flashduty/scripts/incident-summary.sh index b0d64c4..347c453 100644 --- a/skills/flashduty/scripts/incident-summary.sh +++ b/skills/flashduty/scripts/incident-summary.sh @@ -9,7 +9,7 @@ # # Section ⑥ lists recent post-mortems account-wide. To scope them to THIS incident's # channel, read channel_id from the projected detail section and re-run: -# fduty incident post-mortem-list --channel-ids +# flashduty incident post-mortem-list --channel-ids # # Note: errexit (-e) is intentionally NOT set — every section must run even if one # command fails, so the summary stays as complete as possible. Each command's own @@ -25,7 +25,7 @@ fi # Project detail explicitly because its default table includes unbounded narrative # fields. The other read verbs use their compact default renderers; raw toon would # dump every empty field plus heavy blobs like a change's labels.steps. -run() { echo "===== fduty $* ====="; fduty "$@" 2>&1; echo; } +run() { echo "===== flashduty $* ====="; flashduty "$@" 2>&1; echo; } run incident detail "$ID" --fields incident_id,num,title,incident_severity,progress,ai_summary,root_cause,resolution,alert_cnt,start_time,channel_id,detail_url --output-format toon # ① 详情 + AI summary + alert counts + channel run incident alerts "$ID" # ② contributing alerts @@ -38,7 +38,7 @@ run change list --since 24h # ③ correlated changes (shared l # start_time falls within ±15 min of this one's. Alert grouping runs per channel, so one root # cause spanning several services/channels arrives as several incidents; this is the only # section that looks sideways at them. The reference incident itself appears in the list. -START_RAW=$(fduty incident detail "$ID" --json 2>/dev/null | jq -r '.start_time // empty') +START_RAW=$(flashduty incident detail "$ID" --json 2>/dev/null | jq -r '.start_time // empty') START_TS="" if [ -n "$START_RAW" ]; then # The CLI renders start_time as RFC3339 in its local zone. GNU date (sandbox / Linux runners) @@ -51,6 +51,6 @@ if [ -n "$START_TS" ]; then echo "# ⑦ concurrent incidents: every incident in this account (any channel, any progress) started within ±15 min of $START_RAW — includes $ID itself; the trailing note carries the window total" run incident list --since "$((START_TS - 900))" --until "$((START_TS + 900))" --limit 50 --fields incident_id,num,title,incident_severity,progress,start_time,channel_id --output-format toon else - echo "===== ⑦ concurrent incidents: SKIPPED — could not read start_time of $ID; run: fduty incident list --since --until --limit 50 --output-format toon =====" + echo "===== ⑦ concurrent incidents: SKIPPED — could not read start_time of $ID; run: flashduty incident list --since --until --limit 50 --output-format toon =====" echo fi diff --git a/tests/install_ps1_test.ps1 b/tests/install_ps1_test.ps1 new file mode 100644 index 0000000..353d532 --- /dev/null +++ b/tests/install_ps1_test.ps1 @@ -0,0 +1,171 @@ +# Behavior tests for install.ps1. Hermetic: release archives are built locally +# from a stub program and served by an Invoke-WebRequest stand-in, so no +# network is touched. Needs Windows and Go on PATH. +$ErrorActionPreference = "Stop" + +$Root = Split-Path -Parent $PSScriptRoot +$InstallScript = Join-Path $Root "install.ps1" +$TmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "install-ps1-test-$([System.Guid]::NewGuid().ToString('N'))" +$Fixtures = Join-Path $TmpDir "fixtures" +New-Item -ItemType Directory -Path $Fixtures -Force | Out-Null + +$Arch = switch ($env:PROCESSOR_ARCHITECTURE) { + "AMD64" { "x86_64" } + "ARM64" { "arm64" } + default { throw "unsupported architecture: $env:PROCESSOR_ARCHITECTURE" } +} +$Archive = "flashduty-cli_Windows_${Arch}.zip" + +# Stub CLI: prints its build version; `sleep` keeps it running so the +# installed .exe is locked while a reinstall happens. +$StubSource = Join-Path $TmpDir "stub.go" +Set-Content -Path $StubSource -Encoding ascii -Value @' +package main + +import ( + "fmt" + "os" + "time" +) + +var version = "dev" + +func main() { + if len(os.Args) > 1 && os.Args[1] == "sleep" { + time.Sleep(10 * time.Minute) + } + fmt.Println("stub " + version) +} +'@ + +# Each release is //{archive, checksums.txt}, the layout +# install.ps1 downloads from /releases/download//. +function New-Release($Version) { + $dir = Join-Path $Fixtures $Version + $build = Join-Path $dir "build" + New-Item -ItemType Directory -Path $build -Force | Out-Null + & go build -ldflags "-X main.version=$Version" -o (Join-Path $build "flashduty-cli.exe") $StubSource + if ($LASTEXITCODE -ne 0) { throw "go build failed for $Version" } + $zip = Join-Path $dir $Archive + Compress-Archive -Path (Join-Path $build "flashduty-cli.exe") -DestinationPath $zip -Force + $sum = (Get-FileHash -Path $zip -Algorithm SHA256).Hash.ToLower() + Set-Content -Path (Join-Path $dir "checksums.txt") -Encoding ascii -Value "$sum $Archive" +} + +# Stand-in for the cmdlet: functions take precedence over cmdlets, so the +# installer run below in this session downloads from the local fixtures. +function Invoke-WebRequest { + param([string]$Uri, [string]$OutFile, [switch]$UseBasicParsing) + if ($Uri -notmatch '/releases/download/([^/]+)/([^/]+)$') { throw "unexpected URL: $Uri" } + $src = Join-Path (Join-Path $Fixtures $Matches[1]) $Matches[2] + if (-not (Test-Path $src)) { throw "no fixture for URL: $Uri" } + Copy-Item -Path $src -Destination $OutFile -Force +} + +function Invoke-Installer($InstallDir, $Version, $InstalledName) { + $env:MIRROR_URL = "https://mirror.example/flashduty-cli" + $env:FLASHDUTY_VERSION = $Version + $env:FLASHDUTY_INSTALL_DIR = $InstallDir + $env:INSTALLED_NAME = $InstalledName + & $InstallScript | Out-Null +} + +function Assert-Runs($Exe, $Version) { + $out = (& $Exe | Out-String).Trim() + if ($out -ne "stub $Version") { throw "$Exe printed '$out', want 'stub $Version'" } +} + +function Assert-ExactName($Dir, $Name) { + $names = @(Get-ChildItem -Path $Dir -File | Where-Object { $_.Name -notlike "*.old" } | ForEach-Object { $_.Name }) + if ($names.Count -ne 1 -or $names[0] -cne $Name) { + throw "files in ${Dir}: [$($names -join ', ')], want exactly [$Name]" + } +} + +function Get-OldCopies($Dir, $Name) { + @(Get-ChildItem -Path $Dir -File -Filter "$Name*.old") +} + +function Test-Case($Name, [scriptblock]$Body) { + try { + & $Body + Write-Host "PASS: $Name" + } catch { + Write-Host "FAIL: $Name -- $_" + $script:Failed++ + } +} + +$Failed = 0 +$Sleepers = @() +$SavedUserPath = [Environment]::GetEnvironmentVariable("Path", "User") +try { + New-Release "v1.0.0" + New-Release "v2.0.0" + New-Release "v3.0.0" + + Test-Case "default name installs flashduty.exe" { + $dir = Join-Path $TmpDir "default" + Invoke-Installer $dir "v1.0.0" "" + Assert-ExactName $dir "flashduty.exe" + Assert-Runs (Join-Path $dir "flashduty.exe") "v1.0.0" + } + + Test-Case "INSTALLED_NAME=fduty installs fduty.exe" { + $dir = Join-Path $TmpDir "fduty" + Invoke-Installer $dir "v1.0.0" "fduty" + Assert-ExactName $dir "fduty.exe" + Assert-Runs (Join-Path $dir "fduty.exe") "v1.0.0" + } + + Test-Case "INSTALLED_NAME=FDUTY.EXE installs FDUTY.exe" { + $dir = Join-Path $TmpDir "fduty-upper" + Invoke-Installer $dir "v1.0.0" "FDUTY.EXE" + Assert-ExactName $dir "FDUTY.exe" + Assert-Runs (Join-Path $dir "FDUTY.exe") "v1.0.0" + } + + $runDir = Join-Path $TmpDir "running" + $runExe = Join-Path $runDir "flashduty.exe" + + Test-Case "reinstall replaces the installed exe while it is running" { + Invoke-Installer $runDir "v1.0.0" "" + $script:Sleepers += Start-Process -FilePath $runExe -ArgumentList "sleep" -PassThru -WindowStyle Hidden + Start-Sleep -Seconds 1 + Invoke-Installer $runDir "v2.0.0" "" + Assert-Runs $runExe "v2.0.0" + if ($script:Sleepers[-1].HasExited) { throw "running v1.0.0 process exited during reinstall" } + if ((Get-OldCopies $runDir "flashduty.exe").Count -ne 1) { throw "want one moved-aside copy of the running exe" } + } + + Test-Case "reinstall while the moved-aside copy is still running" { + # The v1.0.0 process from the previous case still runs from the + # moved-aside file, which therefore cannot be deleted or replaced. + $script:Sleepers += Start-Process -FilePath $runExe -ArgumentList "sleep" -PassThru -WindowStyle Hidden + Start-Sleep -Seconds 1 + Invoke-Installer $runDir "v3.0.0" "" + Assert-Runs $runExe "v3.0.0" + Assert-ExactName $runDir "flashduty.exe" + } + + Test-Case "a stale moved-aside copy from an earlier install is cleaned up" { + $dir = Join-Path $TmpDir "stale" + Invoke-Installer $dir "v1.0.0" "" + Invoke-Installer $dir "v2.0.0" "" + Invoke-Installer $dir "v3.0.0" "" + Assert-Runs (Join-Path $dir "flashduty.exe") "v3.0.0" + $old = Get-OldCopies $dir "flashduty.exe" + if ($old.Count -ne 1) { throw "want one moved-aside copy after three installs, got $($old.Count): $($old.Name -join ', ')" } + } +} finally { + $Sleepers | Where-Object { -not $_.HasExited } | Stop-Process -Force -ErrorAction SilentlyContinue + [Environment]::SetEnvironmentVariable("Path", $SavedUserPath, "User") + Start-Sleep -Seconds 1 + Remove-Item -Path $TmpDir -Recurse -Force -ErrorAction SilentlyContinue +} + +if ($Failed -gt 0) { + Write-Host "$Failed case(s) failed" + exit 1 +} +Write-Host "all install.ps1 cases passed"