refactor(sdk): remplace dépôt imbriqué par git submodule
- Convertit sdk/IdeaSDK en git submodule pointant vers IdeaSDK dédié - Nettoie les artefacts du dépôt git imbriqué (.git.disabled-empty-nested-repo) - Le SDK TypeScript plugin est maintenant dans son propre repo Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[submodule "sdk/IdeaSDK"]
|
||||||
|
path = sdk/IdeaSDK
|
||||||
|
url = https://gitea.anthonybouteiller.ovh/blomios/IdeaSDK.git
|
||||||
1
sdk/IdeaSDK
Submodule
1
sdk/IdeaSDK
Submodule
Submodule sdk/IdeaSDK added at c322055edb
@ -1 +0,0 @@
|
|||||||
ref: refs/heads/main
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
[core]
|
|
||||||
repositoryformatversion = 0
|
|
||||||
filemode = true
|
|
||||||
bare = false
|
|
||||||
logallrefupdates = true
|
|
||||||
[remote "origin"]
|
|
||||||
url = https://gitea.anthonybouteiller.ovh/blomios/IdeaSDK.git
|
|
||||||
fetch = +refs/heads/*:refs/remotes/origin/*
|
|
||||||
[branch "main"]
|
|
||||||
remote = origin
|
|
||||||
merge = refs/heads/main
|
|
||||||
@ -1 +0,0 @@
|
|||||||
Unnamed repository; edit this file 'description' to name the repository.
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# An example hook script to check the commit log message taken by
|
|
||||||
# applypatch from an e-mail message.
|
|
||||||
#
|
|
||||||
# The hook should exit with non-zero status after issuing an
|
|
||||||
# appropriate message if it wants to stop the commit. The hook is
|
|
||||||
# allowed to edit the commit message file.
|
|
||||||
#
|
|
||||||
# To enable this hook, rename this file to "applypatch-msg".
|
|
||||||
|
|
||||||
. git-sh-setup
|
|
||||||
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
|
|
||||||
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
|
|
||||||
:
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# An example hook script to check the commit log message.
|
|
||||||
# Called by "git commit" with one argument, the name of the file
|
|
||||||
# that has the commit message. The hook should exit with non-zero
|
|
||||||
# status after issuing an appropriate message if it wants to stop the
|
|
||||||
# commit. The hook is allowed to edit the commit message file.
|
|
||||||
#
|
|
||||||
# To enable this hook, rename this file to "commit-msg".
|
|
||||||
|
|
||||||
# Uncomment the below to add a Signed-off-by line to the message.
|
|
||||||
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
|
|
||||||
# hook is more suited to it.
|
|
||||||
#
|
|
||||||
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
|
||||||
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
|
|
||||||
|
|
||||||
# This example catches duplicate Signed-off-by lines and messages that
|
|
||||||
# would confuse 'git am'.
|
|
||||||
|
|
||||||
ret=0
|
|
||||||
|
|
||||||
test "" = "$(grep '^Signed-off-by: ' "$1" |
|
|
||||||
sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
|
|
||||||
echo >&2 Duplicate Signed-off-by lines.
|
|
||||||
ret=1
|
|
||||||
}
|
|
||||||
|
|
||||||
comment_re="$(
|
|
||||||
{
|
|
||||||
git config --get-regexp "^core\.comment(char|string)\$" ||
|
|
||||||
echo '#'
|
|
||||||
} | sed -n -e '
|
|
||||||
${
|
|
||||||
s/^[^ ]* //
|
|
||||||
s|[][*./\]|\\&|g
|
|
||||||
s/^auto$/[#;@!$%^&|:]/
|
|
||||||
p
|
|
||||||
}'
|
|
||||||
)"
|
|
||||||
scissors_line="^${comment_re} -\{8,\} >8 -\{8,\}\$"
|
|
||||||
comment_line="^${comment_re}.*"
|
|
||||||
blank_line='^[ ]*$'
|
|
||||||
# Disallow lines starting with "diff -" or "Index: " in the body of the
|
|
||||||
# message. Stop looking if we see a scissors line.
|
|
||||||
line="$(sed -n -e "
|
|
||||||
# Skip comments and blank lines at the start of the file.
|
|
||||||
/${scissors_line}/q
|
|
||||||
/${comment_line}/d
|
|
||||||
/${blank_line}/d
|
|
||||||
# The first paragraph will become the subject header so
|
|
||||||
# does not need to be checked.
|
|
||||||
: subject
|
|
||||||
n
|
|
||||||
/${scissors_line}/q
|
|
||||||
/${blank_line}/!b subject
|
|
||||||
# Check the body of the message for problematic
|
|
||||||
# prefixes.
|
|
||||||
: body
|
|
||||||
n
|
|
||||||
/${scissors_line}/q
|
|
||||||
/${comment_line}/b body
|
|
||||||
/^diff -/{p;q;}
|
|
||||||
/^Index: /{p;q;}
|
|
||||||
b body
|
|
||||||
" "$1")"
|
|
||||||
if test -n "$line"
|
|
||||||
then
|
|
||||||
echo >&2 "Message contains a diff that will confuse 'git am'."
|
|
||||||
echo >&2 "To fix this indent the diff."
|
|
||||||
ret=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit $ret
|
|
||||||
@ -1,168 +0,0 @@
|
|||||||
#!/usr/bin/perl
|
|
||||||
|
|
||||||
use strict;
|
|
||||||
use warnings;
|
|
||||||
use IPC::Open2;
|
|
||||||
|
|
||||||
# An example hook script to integrate Watchman
|
|
||||||
# (https://facebook.github.io/watchman/) with git to speed up detecting
|
|
||||||
# new and modified files.
|
|
||||||
#
|
|
||||||
# The hook is passed a version (currently 2) and last update token
|
|
||||||
# formatted as a string and outputs to stdout a new update token and
|
|
||||||
# all files that have been modified since the update token. Paths must
|
|
||||||
# be relative to the root of the working tree and separated by a single NUL.
|
|
||||||
#
|
|
||||||
# To enable this hook, rename this file to "query-watchman" and set
|
|
||||||
# 'git config core.fsmonitor .git/hooks/query-watchman'
|
|
||||||
#
|
|
||||||
my ($version, $last_update_token) = @ARGV;
|
|
||||||
|
|
||||||
# Uncomment for debugging
|
|
||||||
# print STDERR "$0 $version $last_update_token\n";
|
|
||||||
|
|
||||||
# Check the hook interface version
|
|
||||||
if ($version ne 2) {
|
|
||||||
die "Unsupported query-fsmonitor hook version '$version'.\n" .
|
|
||||||
"Falling back to scanning...\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
my $git_work_tree = get_working_dir();
|
|
||||||
|
|
||||||
my $json_pkg;
|
|
||||||
eval {
|
|
||||||
require JSON::XS;
|
|
||||||
$json_pkg = "JSON::XS";
|
|
||||||
1;
|
|
||||||
} or do {
|
|
||||||
require JSON::PP;
|
|
||||||
$json_pkg = "JSON::PP";
|
|
||||||
};
|
|
||||||
|
|
||||||
launch_watchman();
|
|
||||||
|
|
||||||
sub launch_watchman {
|
|
||||||
my $o = watchman_query();
|
|
||||||
if (is_work_tree_watched($o)) {
|
|
||||||
output_result($o->{clock}, @{$o->{files}});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sub output_result {
|
|
||||||
my ($clockid, @files) = @_;
|
|
||||||
|
|
||||||
# Uncomment for debugging watchman output
|
|
||||||
# open (my $fh, ">", ".git/watchman-output.out");
|
|
||||||
# binmode $fh, ":utf8";
|
|
||||||
# print $fh "$clockid\n@files\n";
|
|
||||||
# close $fh;
|
|
||||||
|
|
||||||
binmode STDOUT, ":utf8";
|
|
||||||
print $clockid;
|
|
||||||
print "\0";
|
|
||||||
local $, = "\0";
|
|
||||||
print @files;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub watchman_clock {
|
|
||||||
my $response = qx/watchman clock "$git_work_tree"/;
|
|
||||||
die "Failed to get clock id on '$git_work_tree'.\n" .
|
|
||||||
"Falling back to scanning...\n" if $? != 0;
|
|
||||||
|
|
||||||
return $json_pkg->new->utf8->decode($response);
|
|
||||||
}
|
|
||||||
|
|
||||||
sub watchman_query {
|
|
||||||
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
|
|
||||||
or die "open2() failed: $!\n" .
|
|
||||||
"Falling back to scanning...\n";
|
|
||||||
|
|
||||||
# In the query expression below we're asking for names of files that
|
|
||||||
# changed since $last_update_token but not from the .git folder.
|
|
||||||
#
|
|
||||||
# To accomplish this, we're using the "since" generator to use the
|
|
||||||
# recency index to select candidate nodes and "fields" to limit the
|
|
||||||
# output to file names only. Then we're using the "expression" term to
|
|
||||||
# further constrain the results.
|
|
||||||
my $last_update_line = "";
|
|
||||||
if (substr($last_update_token, 0, 1) eq "c") {
|
|
||||||
$last_update_token = "\"$last_update_token\"";
|
|
||||||
$last_update_line = qq[\n"since": $last_update_token,];
|
|
||||||
}
|
|
||||||
my $query = <<" END";
|
|
||||||
["query", "$git_work_tree", {$last_update_line
|
|
||||||
"fields": ["name"],
|
|
||||||
"expression": ["not", ["dirname", ".git"]]
|
|
||||||
}]
|
|
||||||
END
|
|
||||||
|
|
||||||
# Uncomment for debugging the watchman query
|
|
||||||
# open (my $fh, ">", ".git/watchman-query.json");
|
|
||||||
# print $fh $query;
|
|
||||||
# close $fh;
|
|
||||||
|
|
||||||
print CHLD_IN $query;
|
|
||||||
close CHLD_IN;
|
|
||||||
my $response = do {local $/; <CHLD_OUT>};
|
|
||||||
|
|
||||||
# Uncomment for debugging the watch response
|
|
||||||
# open ($fh, ">", ".git/watchman-response.json");
|
|
||||||
# print $fh $response;
|
|
||||||
# close $fh;
|
|
||||||
|
|
||||||
die "Watchman: command returned no output.\n" .
|
|
||||||
"Falling back to scanning...\n" if $response eq "";
|
|
||||||
die "Watchman: command returned invalid output: $response\n" .
|
|
||||||
"Falling back to scanning...\n" unless $response =~ /^\{/;
|
|
||||||
|
|
||||||
return $json_pkg->new->utf8->decode($response);
|
|
||||||
}
|
|
||||||
|
|
||||||
sub is_work_tree_watched {
|
|
||||||
my ($output) = @_;
|
|
||||||
my $error = $output->{error};
|
|
||||||
if ($error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
|
|
||||||
my $response = qx/watchman watch "$git_work_tree"/;
|
|
||||||
die "Failed to make watchman watch '$git_work_tree'.\n" .
|
|
||||||
"Falling back to scanning...\n" if $? != 0;
|
|
||||||
$output = $json_pkg->new->utf8->decode($response);
|
|
||||||
$error = $output->{error};
|
|
||||||
die "Watchman: $error.\n" .
|
|
||||||
"Falling back to scanning...\n" if $error;
|
|
||||||
|
|
||||||
# Uncomment for debugging watchman output
|
|
||||||
# open (my $fh, ">", ".git/watchman-output.out");
|
|
||||||
# close $fh;
|
|
||||||
|
|
||||||
# Watchman will always return all files on the first query so
|
|
||||||
# return the fast "everything is dirty" flag to git and do the
|
|
||||||
# Watchman query just to get it over with now so we won't pay
|
|
||||||
# the cost in git to look up each individual file.
|
|
||||||
my $o = watchman_clock();
|
|
||||||
$error = $o->{error};
|
|
||||||
|
|
||||||
die "Watchman: $error.\n" .
|
|
||||||
"Falling back to scanning...\n" if $error;
|
|
||||||
|
|
||||||
output_result($o->{clock}, ("/"));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
die "Watchman: $error.\n" .
|
|
||||||
"Falling back to scanning...\n" if $error;
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub get_working_dir {
|
|
||||||
my $working_dir;
|
|
||||||
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
|
|
||||||
$working_dir = Win32::GetCwd();
|
|
||||||
$working_dir =~ tr/\\/\//;
|
|
||||||
} else {
|
|
||||||
require Cwd;
|
|
||||||
$working_dir = Cwd::cwd();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $working_dir;
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# An example hook script to prepare a packed repository for use over
|
|
||||||
# dumb transports.
|
|
||||||
#
|
|
||||||
# To enable this hook, rename this file to "post-update".
|
|
||||||
|
|
||||||
exec git update-server-info
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# An example hook script to verify what is about to be committed
|
|
||||||
# by applypatch from an e-mail message.
|
|
||||||
#
|
|
||||||
# The hook should exit with non-zero status after issuing an
|
|
||||||
# appropriate message if it wants to stop the commit.
|
|
||||||
#
|
|
||||||
# To enable this hook, rename this file to "pre-applypatch".
|
|
||||||
|
|
||||||
. git-sh-setup
|
|
||||||
precommit="$(git rev-parse --git-path hooks/pre-commit)"
|
|
||||||
test -x "$precommit" && exec "$precommit" ${1+"$@"}
|
|
||||||
:
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# An example hook script to verify what is about to be committed.
|
|
||||||
# Called by "git commit" with no arguments. The hook should
|
|
||||||
# exit with non-zero status after issuing an appropriate message if
|
|
||||||
# it wants to stop the commit.
|
|
||||||
#
|
|
||||||
# To enable this hook, rename this file to "pre-commit".
|
|
||||||
|
|
||||||
if git rev-parse --verify HEAD >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
against=HEAD
|
|
||||||
else
|
|
||||||
# Initial commit: diff against an empty tree object
|
|
||||||
against=$(git hash-object -t tree /dev/null)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# If you want to allow non-ASCII filenames set this variable to true.
|
|
||||||
allownonascii=$(git config --type=bool hooks.allownonascii)
|
|
||||||
|
|
||||||
# Redirect output to stderr.
|
|
||||||
exec 1>&2
|
|
||||||
|
|
||||||
# Cross platform projects tend to avoid non-ASCII filenames; prevent
|
|
||||||
# them from being added to the repository. We exploit the fact that the
|
|
||||||
# printable range starts at the space character and ends with tilde.
|
|
||||||
if [ "$allownonascii" != "true" ] &&
|
|
||||||
# Note that the use of brackets around a tr range is ok here, (it's
|
|
||||||
# even required, for portability to Solaris 10's /usr/bin/tr), since
|
|
||||||
# the square bracket bytes happen to fall in the designated range.
|
|
||||||
test $(git diff-index --cached --name-only --diff-filter=A -z $against |
|
|
||||||
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
|
|
||||||
then
|
|
||||||
cat <<\EOF
|
|
||||||
Error: Attempt to add a non-ASCII file name.
|
|
||||||
|
|
||||||
This can cause problems if you want to work with people on other platforms.
|
|
||||||
|
|
||||||
To be portable it is advisable to rename the file.
|
|
||||||
|
|
||||||
If you know what you are doing you can disable this check using:
|
|
||||||
|
|
||||||
git config hooks.allownonascii true
|
|
||||||
EOF
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# If there are whitespace errors, print the offending file names and fail.
|
|
||||||
exec git diff-index --check --cached $against --
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# An example hook script to verify what is about to be committed.
|
|
||||||
# Called by "git merge" with no arguments. The hook should
|
|
||||||
# exit with non-zero status after issuing an appropriate message to
|
|
||||||
# stderr if it wants to stop the merge commit.
|
|
||||||
#
|
|
||||||
# To enable this hook, rename this file to "pre-merge-commit".
|
|
||||||
|
|
||||||
. git-sh-setup
|
|
||||||
test -x "$GIT_DIR/hooks/pre-commit" &&
|
|
||||||
exec "$GIT_DIR/hooks/pre-commit"
|
|
||||||
:
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# An example hook script to verify what is about to be pushed. Called by "git
|
|
||||||
# push" after it has checked the remote status, but before anything has been
|
|
||||||
# pushed. If this script exits with a non-zero status nothing will be pushed.
|
|
||||||
#
|
|
||||||
# This hook is called with the following parameters:
|
|
||||||
#
|
|
||||||
# $1 -- Name of the remote to which the push is being done
|
|
||||||
# $2 -- URL to which the push is being done
|
|
||||||
#
|
|
||||||
# If pushing without using a named remote those arguments will be equal.
|
|
||||||
#
|
|
||||||
# Information about the commits which are being pushed is supplied as lines to
|
|
||||||
# the standard input in the form:
|
|
||||||
#
|
|
||||||
# <local ref> <local oid> <remote ref> <remote oid>
|
|
||||||
#
|
|
||||||
# This sample shows how to prevent push of commits where the log message starts
|
|
||||||
# with "WIP" (work in progress).
|
|
||||||
|
|
||||||
remote="$1"
|
|
||||||
url="$2"
|
|
||||||
|
|
||||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
|
||||||
|
|
||||||
while read local_ref local_oid remote_ref remote_oid
|
|
||||||
do
|
|
||||||
if test "$local_oid" = "$zero"
|
|
||||||
then
|
|
||||||
# Handle delete
|
|
||||||
:
|
|
||||||
else
|
|
||||||
if test "$remote_oid" = "$zero"
|
|
||||||
then
|
|
||||||
# New branch, examine all commits
|
|
||||||
range="$local_oid"
|
|
||||||
else
|
|
||||||
# Update to existing branch, examine new commits
|
|
||||||
range="$remote_oid..$local_oid"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check for WIP commit
|
|
||||||
commit=$(git rev-list -n 1 --grep '^WIP' "$range")
|
|
||||||
if test -n "$commit"
|
|
||||||
then
|
|
||||||
echo >&2 "Found WIP commit in $local_ref, not pushing"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# Copyright (c) 2006, 2008 Junio C Hamano
|
|
||||||
#
|
|
||||||
# The "pre-rebase" hook is run just before "git rebase" starts doing
|
|
||||||
# its job, and can prevent the command from running by exiting with
|
|
||||||
# non-zero status.
|
|
||||||
#
|
|
||||||
# The hook is called with the following parameters:
|
|
||||||
#
|
|
||||||
# $1 -- the upstream the series was forked from.
|
|
||||||
# $2 -- the branch being rebased (or empty when rebasing the current branch).
|
|
||||||
#
|
|
||||||
# This sample shows how to prevent topic branches that are already
|
|
||||||
# merged to 'next' branch from getting rebased, because allowing it
|
|
||||||
# would result in rebasing already published history.
|
|
||||||
|
|
||||||
publish=next
|
|
||||||
basebranch="$1"
|
|
||||||
if test "$#" = 2
|
|
||||||
then
|
|
||||||
topic="refs/heads/$2"
|
|
||||||
else
|
|
||||||
topic=`git symbolic-ref HEAD` ||
|
|
||||||
exit 0 ;# we do not interrupt rebasing detached HEAD
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$topic" in
|
|
||||||
refs/heads/??/*)
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
exit 0 ;# we do not interrupt others.
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Now we are dealing with a topic branch being rebased
|
|
||||||
# on top of master. Is it OK to rebase it?
|
|
||||||
|
|
||||||
# Does the topic really exist?
|
|
||||||
git show-ref -q "$topic" || {
|
|
||||||
echo >&2 "No such branch $topic"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Is topic fully merged to master?
|
|
||||||
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
|
|
||||||
if test -z "$not_in_master"
|
|
||||||
then
|
|
||||||
echo >&2 "$topic is fully merged to master; better remove it."
|
|
||||||
exit 1 ;# we could allow it, but there is no point.
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Is topic ever merged to next? If so you should not be rebasing it.
|
|
||||||
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
|
|
||||||
only_next_2=`git rev-list ^master ${publish} | sort`
|
|
||||||
if test "$only_next_1" = "$only_next_2"
|
|
||||||
then
|
|
||||||
not_in_topic=`git rev-list "^$topic" master`
|
|
||||||
if test -z "$not_in_topic"
|
|
||||||
then
|
|
||||||
echo >&2 "$topic is already up to date with master"
|
|
||||||
exit 1 ;# we could allow it, but there is no point.
|
|
||||||
else
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
|
|
||||||
/usr/bin/perl -e '
|
|
||||||
my $topic = $ARGV[0];
|
|
||||||
my $msg = "* $topic has commits already merged to public branch:\n";
|
|
||||||
my (%not_in_next) = map {
|
|
||||||
/^([0-9a-f]+) /;
|
|
||||||
($1 => 1);
|
|
||||||
} split(/\n/, $ARGV[1]);
|
|
||||||
for my $elem (map {
|
|
||||||
/^([0-9a-f]+) (.*)$/;
|
|
||||||
[$1 => $2];
|
|
||||||
} split(/\n/, $ARGV[2])) {
|
|
||||||
if (!exists $not_in_next{$elem->[0]}) {
|
|
||||||
if ($msg) {
|
|
||||||
print STDERR $msg;
|
|
||||||
undef $msg;
|
|
||||||
}
|
|
||||||
print STDERR " $elem->[1]\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
' "$topic" "$not_in_next" "$not_in_master"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
<<\DOC_END
|
|
||||||
|
|
||||||
This sample hook safeguards topic branches that have been
|
|
||||||
published from being rewound.
|
|
||||||
|
|
||||||
The workflow assumed here is:
|
|
||||||
|
|
||||||
* Once a topic branch forks from "master", "master" is never
|
|
||||||
merged into it again (either directly or indirectly).
|
|
||||||
|
|
||||||
* Once a topic branch is fully cooked and merged into "master",
|
|
||||||
it is deleted. If you need to build on top of it to correct
|
|
||||||
earlier mistakes, a new topic branch is created by forking at
|
|
||||||
the tip of the "master". This is not strictly necessary, but
|
|
||||||
it makes it easier to keep your history simple.
|
|
||||||
|
|
||||||
* Whenever you need to test or publish your changes to topic
|
|
||||||
branches, merge them into "next" branch.
|
|
||||||
|
|
||||||
The script, being an example, hardcodes the publish branch name
|
|
||||||
to be "next", but it is trivial to make it configurable via
|
|
||||||
$GIT_DIR/config mechanism.
|
|
||||||
|
|
||||||
With this workflow, you would want to know:
|
|
||||||
|
|
||||||
(1) ... if a topic branch has ever been merged to "next". Young
|
|
||||||
topic branches can have stupid mistakes you would rather
|
|
||||||
clean up before publishing, and things that have not been
|
|
||||||
merged into other branches can be easily rebased without
|
|
||||||
affecting other people. But once it is published, you would
|
|
||||||
not want to rewind it.
|
|
||||||
|
|
||||||
(2) ... if a topic branch has been fully merged to "master".
|
|
||||||
Then you can delete it. More importantly, you should not
|
|
||||||
build on top of it -- other people may already want to
|
|
||||||
change things related to the topic as patches against your
|
|
||||||
"master", so if you need further changes, it is better to
|
|
||||||
fork the topic (perhaps with the same name) afresh from the
|
|
||||||
tip of "master".
|
|
||||||
|
|
||||||
Let's look at this example:
|
|
||||||
|
|
||||||
o---o---o---o---o---o---o---o---o---o "next"
|
|
||||||
/ / / /
|
|
||||||
/ a---a---b A / /
|
|
||||||
/ / / /
|
|
||||||
/ / c---c---c---c B /
|
|
||||||
/ / / \ /
|
|
||||||
/ / / b---b C \ /
|
|
||||||
/ / / / \ /
|
|
||||||
---o---o---o---o---o---o---o---o---o---o---o "master"
|
|
||||||
|
|
||||||
|
|
||||||
A, B and C are topic branches.
|
|
||||||
|
|
||||||
* A has one fix since it was merged up to "next".
|
|
||||||
|
|
||||||
* B has finished. It has been fully merged up to "master" and "next",
|
|
||||||
and is ready to be deleted.
|
|
||||||
|
|
||||||
* C has not merged to "next" at all.
|
|
||||||
|
|
||||||
We would want to allow C to be rebased, refuse A, and encourage
|
|
||||||
B to be deleted.
|
|
||||||
|
|
||||||
To compute (1):
|
|
||||||
|
|
||||||
git rev-list ^master ^topic next
|
|
||||||
git rev-list ^master next
|
|
||||||
|
|
||||||
if these match, topic has not merged in next at all.
|
|
||||||
|
|
||||||
To compute (2):
|
|
||||||
|
|
||||||
git rev-list master..topic
|
|
||||||
|
|
||||||
if this is empty, it is fully merged to "master".
|
|
||||||
|
|
||||||
DOC_END
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# An example hook script to make use of push options.
|
|
||||||
# The example simply echoes all push options that start with 'echoback='
|
|
||||||
# and rejects all pushes when the "reject" push option is used.
|
|
||||||
#
|
|
||||||
# To enable this hook, rename this file to "pre-receive".
|
|
||||||
|
|
||||||
if test -n "$GIT_PUSH_OPTION_COUNT"
|
|
||||||
then
|
|
||||||
i=0
|
|
||||||
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
|
|
||||||
do
|
|
||||||
eval "value=\$GIT_PUSH_OPTION_$i"
|
|
||||||
case "$value" in
|
|
||||||
echoback=*)
|
|
||||||
echo "echo from the pre-receive-hook: ${value#*=}" >&2
|
|
||||||
;;
|
|
||||||
reject)
|
|
||||||
exit 1
|
|
||||||
esac
|
|
||||||
i=$((i + 1))
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# An example hook script to prepare the commit log message.
|
|
||||||
# Called by "git commit" with the name of the file that has the
|
|
||||||
# commit message, followed by the description of the commit
|
|
||||||
# message's source. The hook's purpose is to edit the commit
|
|
||||||
# message file. If the hook fails with a non-zero status,
|
|
||||||
# the commit is aborted.
|
|
||||||
#
|
|
||||||
# To enable this hook, rename this file to "prepare-commit-msg".
|
|
||||||
|
|
||||||
# This hook includes three examples. The first one removes the
|
|
||||||
# "# Please enter the commit message..." help message.
|
|
||||||
#
|
|
||||||
# The second includes the output of "git diff --name-status -r"
|
|
||||||
# into the message, just before the "git status" output. It is
|
|
||||||
# commented because it doesn't cope with --amend or with squashed
|
|
||||||
# commits.
|
|
||||||
#
|
|
||||||
# The third example adds a Signed-off-by line to the message, that can
|
|
||||||
# still be edited. This is rarely a good idea.
|
|
||||||
|
|
||||||
COMMIT_MSG_FILE=$1
|
|
||||||
COMMIT_SOURCE=$2
|
|
||||||
SHA1=$3
|
|
||||||
|
|
||||||
/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
|
|
||||||
|
|
||||||
# case "$COMMIT_SOURCE,$SHA1" in
|
|
||||||
# ,|template,)
|
|
||||||
# /usr/bin/perl -i.bak -pe '
|
|
||||||
# print "\n" . `git diff --cached --name-status -r`
|
|
||||||
# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
|
|
||||||
# *) ;;
|
|
||||||
# esac
|
|
||||||
|
|
||||||
# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
|
|
||||||
# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
|
|
||||||
# if test -z "$COMMIT_SOURCE"
|
|
||||||
# then
|
|
||||||
# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
|
|
||||||
# fi
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# An example hook script to update a checked-out tree on a git push.
|
|
||||||
#
|
|
||||||
# This hook is invoked by git-receive-pack(1) when it reacts to git
|
|
||||||
# push and updates reference(s) in its repository, and when the push
|
|
||||||
# tries to update the branch that is currently checked out and the
|
|
||||||
# receive.denyCurrentBranch configuration variable is set to
|
|
||||||
# updateInstead.
|
|
||||||
#
|
|
||||||
# By default, such a push is refused if the working tree and the index
|
|
||||||
# of the remote repository has any difference from the currently
|
|
||||||
# checked out commit; when both the working tree and the index match
|
|
||||||
# the current commit, they are updated to match the newly pushed tip
|
|
||||||
# of the branch. This hook is to be used to override the default
|
|
||||||
# behaviour; however the code below reimplements the default behaviour
|
|
||||||
# as a starting point for convenient modification.
|
|
||||||
#
|
|
||||||
# The hook receives the commit with which the tip of the current
|
|
||||||
# branch is going to be updated:
|
|
||||||
commit=$1
|
|
||||||
|
|
||||||
# It can exit with a non-zero status to refuse the push (when it does
|
|
||||||
# so, it must not modify the index or the working tree).
|
|
||||||
die () {
|
|
||||||
echo >&2 "$*"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Or it can make any necessary changes to the working tree and to the
|
|
||||||
# index to bring them to the desired state when the tip of the current
|
|
||||||
# branch is updated to the new commit, and exit with a zero status.
|
|
||||||
#
|
|
||||||
# For example, the hook can simply run git read-tree -u -m HEAD "$1"
|
|
||||||
# in order to emulate git fetch that is run in the reverse direction
|
|
||||||
# with git push, as the two-tree form of git read-tree -u -m is
|
|
||||||
# essentially the same as git switch or git checkout that switches
|
|
||||||
# branches while keeping the local changes in the working tree that do
|
|
||||||
# not interfere with the difference between the branches.
|
|
||||||
|
|
||||||
# The below is a more-or-less exact translation to shell of the C code
|
|
||||||
# for the default behaviour for git's push-to-checkout hook defined in
|
|
||||||
# the push_to_deploy() function in builtin/receive-pack.c.
|
|
||||||
#
|
|
||||||
# Note that the hook will be executed from the repository directory,
|
|
||||||
# not from the working tree, so if you want to perform operations on
|
|
||||||
# the working tree, you will have to adapt your code accordingly, e.g.
|
|
||||||
# by adding "cd .." or using relative paths.
|
|
||||||
|
|
||||||
if ! git update-index -q --ignore-submodules --refresh
|
|
||||||
then
|
|
||||||
die "Up-to-date check failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! git diff-files --quiet --ignore-submodules --
|
|
||||||
then
|
|
||||||
die "Working directory has unstaged changes"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# This is a rough translation of:
|
|
||||||
#
|
|
||||||
# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
|
|
||||||
if git cat-file -e HEAD 2>/dev/null
|
|
||||||
then
|
|
||||||
head=HEAD
|
|
||||||
else
|
|
||||||
head=$(git hash-object -t tree --stdin </dev/null)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! git diff-index --quiet --cached --ignore-submodules $head --
|
|
||||||
then
|
|
||||||
die "Working directory has staged changes"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! git read-tree -u -m "$commit"
|
|
||||||
then
|
|
||||||
die "Could not update working tree to new HEAD"
|
|
||||||
fi
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# An example hook script to validate a patch (and/or patch series) before
|
|
||||||
# sending it via email.
|
|
||||||
#
|
|
||||||
# The hook should exit with non-zero status after issuing an appropriate
|
|
||||||
# message if it wants to prevent the email(s) from being sent.
|
|
||||||
#
|
|
||||||
# To enable this hook, rename this file to "sendemail-validate".
|
|
||||||
#
|
|
||||||
# By default, it will only check that the patch(es) can be applied on top of
|
|
||||||
# the default upstream branch without conflicts in a secondary worktree. After
|
|
||||||
# validation (successful or not) of the last patch of a series, the worktree
|
|
||||||
# will be deleted.
|
|
||||||
#
|
|
||||||
# The following config variables can be set to change the default remote and
|
|
||||||
# remote ref that are used to apply the patches against:
|
|
||||||
#
|
|
||||||
# sendemail.validateRemote (default: origin)
|
|
||||||
# sendemail.validateRemoteRef (default: HEAD)
|
|
||||||
#
|
|
||||||
# Replace the TODO placeholders with appropriate checks according to your
|
|
||||||
# needs.
|
|
||||||
|
|
||||||
validate_cover_letter () {
|
|
||||||
file="$1"
|
|
||||||
# TODO: Replace with appropriate checks (e.g. spell checking).
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_patch () {
|
|
||||||
file="$1"
|
|
||||||
# Ensure that the patch applies without conflicts.
|
|
||||||
git am -3 "$file" || return
|
|
||||||
# TODO: Replace with appropriate checks for this patch
|
|
||||||
# (e.g. checkpatch.pl).
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_series () {
|
|
||||||
# TODO: Replace with appropriate checks for the whole series
|
|
||||||
# (e.g. quick build, coding style checks, etc.).
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
# main -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1
|
|
||||||
then
|
|
||||||
remote=$(git config --default origin --get sendemail.validateRemote) &&
|
|
||||||
ref=$(git config --default HEAD --get sendemail.validateRemoteRef) &&
|
|
||||||
worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) &&
|
|
||||||
git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" &&
|
|
||||||
git config --replace-all sendemail.validateWorktree "$worktree"
|
|
||||||
else
|
|
||||||
worktree=$(git config --get sendemail.validateWorktree)
|
|
||||||
fi || {
|
|
||||||
echo "sendemail-validate: error: failed to prepare worktree" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
unset GIT_DIR GIT_WORK_TREE
|
|
||||||
cd "$worktree" &&
|
|
||||||
|
|
||||||
if grep -q "^diff --git " "$1"
|
|
||||||
then
|
|
||||||
validate_patch "$1"
|
|
||||||
else
|
|
||||||
validate_cover_letter "$1"
|
|
||||||
fi &&
|
|
||||||
|
|
||||||
if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL"
|
|
||||||
then
|
|
||||||
git config --unset-all sendemail.validateWorktree &&
|
|
||||||
trap 'git worktree remove -ff "$worktree"' EXIT &&
|
|
||||||
validate_series
|
|
||||||
fi
|
|
||||||
@ -1,128 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# An example hook script to block unannotated tags from entering.
|
|
||||||
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
|
|
||||||
#
|
|
||||||
# To enable this hook, rename this file to "update".
|
|
||||||
#
|
|
||||||
# Config
|
|
||||||
# ------
|
|
||||||
# hooks.allowunannotated
|
|
||||||
# This boolean sets whether unannotated tags will be allowed into the
|
|
||||||
# repository. By default they won't be.
|
|
||||||
# hooks.allowdeletetag
|
|
||||||
# This boolean sets whether deleting tags will be allowed in the
|
|
||||||
# repository. By default they won't be.
|
|
||||||
# hooks.allowmodifytag
|
|
||||||
# This boolean sets whether a tag may be modified after creation. By default
|
|
||||||
# it won't be.
|
|
||||||
# hooks.allowdeletebranch
|
|
||||||
# This boolean sets whether deleting branches will be allowed in the
|
|
||||||
# repository. By default they won't be.
|
|
||||||
# hooks.denycreatebranch
|
|
||||||
# This boolean sets whether remotely creating branches will be denied
|
|
||||||
# in the repository. By default this is allowed.
|
|
||||||
#
|
|
||||||
|
|
||||||
# --- Command line
|
|
||||||
refname="$1"
|
|
||||||
oldrev="$2"
|
|
||||||
newrev="$3"
|
|
||||||
|
|
||||||
# --- Safety check
|
|
||||||
if [ -z "$GIT_DIR" ]; then
|
|
||||||
echo "Don't run this script from the command line." >&2
|
|
||||||
echo " (if you want, you could supply GIT_DIR then run" >&2
|
|
||||||
echo " $0 <ref> <oldrev> <newrev>)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
|
|
||||||
echo "usage: $0 <ref> <oldrev> <newrev>" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Config
|
|
||||||
allowunannotated=$(git config --type=bool hooks.allowunannotated)
|
|
||||||
allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
|
|
||||||
denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
|
|
||||||
allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
|
|
||||||
allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
|
|
||||||
|
|
||||||
# check for no description
|
|
||||||
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
|
|
||||||
case "$projectdesc" in
|
|
||||||
"Unnamed repository"* | "")
|
|
||||||
echo "*** Project description file hasn't been set" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# --- Check types
|
|
||||||
# if $newrev is 0000...0000, it's a commit to delete a ref.
|
|
||||||
zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
|
|
||||||
if [ "$newrev" = "$zero" ]; then
|
|
||||||
newrev_type=delete
|
|
||||||
else
|
|
||||||
newrev_type=$(git cat-file -t $newrev)
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$refname","$newrev_type" in
|
|
||||||
refs/tags/*,commit)
|
|
||||||
# un-annotated tag
|
|
||||||
short_refname=${refname##refs/tags/}
|
|
||||||
if [ "$allowunannotated" != "true" ]; then
|
|
||||||
echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
|
|
||||||
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
refs/tags/*,delete)
|
|
||||||
# delete tag
|
|
||||||
if [ "$allowdeletetag" != "true" ]; then
|
|
||||||
echo "*** Deleting a tag is not allowed in this repository" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
refs/tags/*,tag)
|
|
||||||
# annotated tag
|
|
||||||
if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
|
|
||||||
then
|
|
||||||
echo "*** Tag '$refname' already exists." >&2
|
|
||||||
echo "*** Modifying a tag is not allowed in this repository." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
refs/heads/*,commit)
|
|
||||||
# branch
|
|
||||||
if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
|
|
||||||
echo "*** Creating a branch is not allowed in this repository" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
refs/heads/*,delete)
|
|
||||||
# delete branch
|
|
||||||
if [ "$allowdeletebranch" != "true" ]; then
|
|
||||||
echo "*** Deleting a branch is not allowed in this repository" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
refs/remotes/*,commit)
|
|
||||||
# tracking branch
|
|
||||||
;;
|
|
||||||
refs/remotes/*,delete)
|
|
||||||
# delete tracking branch
|
|
||||||
if [ "$allowdeletebranch" != "true" ]; then
|
|
||||||
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
# Anything else (is there anything else?)
|
|
||||||
echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# --- Finished
|
|
||||||
exit 0
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
# git ls-files --others --exclude-from=.git/info/exclude
|
|
||||||
# Lines that start with '#' are comments.
|
|
||||||
# For a project mostly in C, the following would be a good set of
|
|
||||||
# exclude patterns (uncomment them if you want to use them):
|
|
||||||
# *.[oa]
|
|
||||||
# *~
|
|
||||||
9
sdk/IdeaSDK/.gitignore
vendored
9
sdk/IdeaSDK/.gitignore
vendored
@ -1,9 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
dist/
|
|
||||||
examples/hello-plugin/dist/
|
|
||||||
examples/hello-plugin/build/
|
|
||||||
*.tsbuildinfo
|
|
||||||
.DS_Store
|
|
||||||
coverage/
|
|
||||||
npm-debug.log*
|
|
||||||
*.tgz
|
|
||||||
@ -1,383 +0,0 @@
|
|||||||
# IdeA Plugin SDK
|
|
||||||
|
|
||||||
Minimal public TypeScript SDK for IdeA plugins.
|
|
||||||
|
|
||||||
This first version intentionally stays small:
|
|
||||||
|
|
||||||
- public manifest types for `idea-plugin.json`;
|
|
||||||
- public runtime types for plugin modules exposing `activate(ctx)`;
|
|
||||||
- a stable `ctx.services` facade for workspace, background task and terminal operations;
|
|
||||||
- public workspace file APIs for reading, writing, listing, stat and path resolution;
|
|
||||||
- a bounded generic project-structure query API;
|
|
||||||
- public command-task APIs for launching and tracking generic tools;
|
|
||||||
- public external-toolchain diagnostics for executables, env vars and files;
|
|
||||||
- public best-effort event subscriptions and workspace watch;
|
|
||||||
- public structured config-document helpers for JSON documents;
|
|
||||||
- a lightweight manifest validator;
|
|
||||||
- a minimal `examples/hello-plugin` plugin.
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Typecheck the example
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run typecheck:examples
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build the installable hello plugin archive
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run package:hello-plugin
|
|
||||||
```
|
|
||||||
|
|
||||||
The archive is written to:
|
|
||||||
|
|
||||||
```text
|
|
||||||
examples/hello-plugin/build/hello-plugin-0.1.0.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
Its ZIP root contains `idea-plugin.json` directly, with no wrapping parent directory. The
|
|
||||||
compiled ESM entrypoint is emitted at `dist/index.js`, matching the manifest `main` field.
|
|
||||||
|
|
||||||
## Plugin shape
|
|
||||||
|
|
||||||
An IdeA plugin ships an `idea-plugin.json` manifest and a JavaScript entrypoint built from
|
|
||||||
TypeScript.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"ideaPluginManifestVersion": 1,
|
|
||||||
"id": "com.example.hello",
|
|
||||||
"displayName": "Hello Plugin",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"trustLevel": "full",
|
|
||||||
"contributes": {}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The entrypoint exports an `activate(ctx)` function:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import type { ActivateContext } from "@idea/plugin-sdk";
|
|
||||||
|
|
||||||
export function activate(ctx: ActivateContext): void {
|
|
||||||
ctx.logger.info("hello from plugin");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Layout Runtime
|
|
||||||
|
|
||||||
Plugins can contribute custom layout panels by declaring `contributes.layouts`
|
|
||||||
in `idea-plugin.json` and registering the matching layout type during
|
|
||||||
`activate(ctx)`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"contributes": {
|
|
||||||
"layouts": [
|
|
||||||
{
|
|
||||||
"type": "com.example.status",
|
|
||||||
"label": "Status",
|
|
||||||
"component": "StatusPanel"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import type { ActivateContext, PluginLayoutProps } from "@idea/plugin-sdk";
|
|
||||||
|
|
||||||
function StatusPanel(props: PluginLayoutProps): string {
|
|
||||||
return `status for ${props.projectId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function activate(ctx: ActivateContext): void {
|
|
||||||
const disposable = ctx.layouts?.register({
|
|
||||||
type: "com.example.status",
|
|
||||||
component: StatusPanel
|
|
||||||
});
|
|
||||||
if (disposable) ctx.subscriptions.push(disposable);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Public layout props are:
|
|
||||||
|
|
||||||
- `projectId`: project hosting the layout cell;
|
|
||||||
- `nodeId`: stable layout node id for that cell instance;
|
|
||||||
- `layoutType`: contributed layout type from the manifest;
|
|
||||||
- `state`: opaque JSON-serializable state persisted by the host;
|
|
||||||
- `setState(next)`: replaces that state;
|
|
||||||
- `availability`: currently `"available"` when the component is mounted.
|
|
||||||
|
|
||||||
Lifecycle: register layouts during `activate(ctx)`, keep the returned disposable
|
|
||||||
in `ctx.subscriptions`, and let the host dispose it on plugin unload. Layout
|
|
||||||
components may be mounted, unmounted and remounted by the host; keep durable UI
|
|
||||||
state in `state` via `setState`, not in module globals. Call `setState` from
|
|
||||||
user actions, effects or asynchronous callbacks, not unconditionally while
|
|
||||||
rendering. Services are available from `ctx.services` to plugins declaring the
|
|
||||||
`tooling` capability; layout props do not expose private runtime gateways.
|
|
||||||
|
|
||||||
## Runtime Services
|
|
||||||
|
|
||||||
Plugins declaring the `tooling` capability receive `ctx.services`. Plugins
|
|
||||||
without that capability do not receive this facade. Prefer `ctx.services` over
|
|
||||||
IdeA's internal runtime objects when it is available:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import type { ActivateContext } from "@idea/plugin-sdk";
|
|
||||||
|
|
||||||
export async function activate(ctx: ActivateContext): Promise<void> {
|
|
||||||
const project = await ctx.services?.workspace.getCurrentProject();
|
|
||||||
ctx.logger.info("current project", project);
|
|
||||||
|
|
||||||
const task = await ctx.services?.tasks.getStatus("task-id");
|
|
||||||
ctx.logger.info("task status", task?.status);
|
|
||||||
|
|
||||||
const terminal = await ctx.services?.terminal.open({ rows: 24, cols: 80 });
|
|
||||||
await terminal?.write(new TextEncoder().encode("echo hello\\r"));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Workspace Files
|
|
||||||
|
|
||||||
Workspace paths are always relative to the project root. Hosts reject absolute
|
|
||||||
paths, `..`, empty path segments and paths outside the sandbox. Text APIs use
|
|
||||||
UTF-8; binary APIs use `Uint8Array`. Missing files reject on reads and resolve
|
|
||||||
to `{ exists: false }` from `stat`.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import type { ActivateContext } from "@idea/plugin-sdk";
|
|
||||||
|
|
||||||
export async function activate(ctx: ActivateContext): Promise<void> {
|
|
||||||
const workspace = ctx.services?.workspace;
|
|
||||||
const project = await workspace?.getCurrentProject();
|
|
||||||
if (!workspace || !project) return;
|
|
||||||
|
|
||||||
await workspace.writeTextFile(".ideai/hello-plugin.txt", "hello\n", project.id);
|
|
||||||
|
|
||||||
const file = await workspace.readTextFile(".ideai/hello-plugin.txt", project.id);
|
|
||||||
const listing = await workspace.listDirectory(".ideai", project.id);
|
|
||||||
const stat = await workspace.stat(file.path, project.id);
|
|
||||||
|
|
||||||
ctx.logger.info("workspace file", {
|
|
||||||
path: file.path,
|
|
||||||
bytes: stat.len,
|
|
||||||
entries: listing.entries.length
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`watch(path, handler, projectId?)` subscribes to public workspace file-change
|
|
||||||
events for the given relative path. It is best-effort and bounded: plugins should
|
|
||||||
handle missed events by refreshing their own derived state when needed.
|
|
||||||
|
|
||||||
### Project Structure
|
|
||||||
|
|
||||||
`queryStructure()` returns a bounded, generic read model so plugins do not each
|
|
||||||
need to rescan the whole workspace for common markers:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const structure = await ctx.services?.workspace.queryStructure({
|
|
||||||
maxDepth: 3,
|
|
||||||
maxEntries: 500
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const convention of structure?.conventions ?? []) {
|
|
||||||
console.log(convention.id, convention.markerPath);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The MVP detects generic marker-file conventions such as `package.json`,
|
|
||||||
`Cargo.toml`, `pyproject.toml`, `go.mod`, `Makefile` and `.git`. It deliberately
|
|
||||||
does not expose language-specific ASTs or Android-specific concepts.
|
|
||||||
|
|
||||||
Current terminal scope is intentionally minimal: it opens or reattaches a shell
|
|
||||||
PTY, writes bytes, resizes, detaches and closes.
|
|
||||||
|
|
||||||
### Command Tasks
|
|
||||||
|
|
||||||
Use `ctx.services.tasks.runCommand()` for non-interactive tools that should be
|
|
||||||
tracked as IdeA background tasks instead of opening a raw PTY. `command` and
|
|
||||||
`args` are passed separately, `cwd` is relative to the project root, and `env`
|
|
||||||
adds process environment variables. The current host requires an `ownerAgentId`
|
|
||||||
so the task can appear in Work and completion can be correlated to an agent.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import type { ActivateContext } from "@idea/plugin-sdk";
|
|
||||||
|
|
||||||
export async function activate(ctx: ActivateContext): Promise<void> {
|
|
||||||
const project = await ctx.services?.workspace.getCurrentProject();
|
|
||||||
if (!project) return;
|
|
||||||
|
|
||||||
const task = await ctx.services?.tasks.runCommand({
|
|
||||||
projectId: project.id,
|
|
||||||
ownerAgentId: "00000000-0000-0000-0000-000000000000",
|
|
||||||
label: "Check npm",
|
|
||||||
command: "npm",
|
|
||||||
args: ["--version"],
|
|
||||||
cwd: ".",
|
|
||||||
env: { CI: "1" },
|
|
||||||
recordOnly: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const status = await ctx.services?.tasks.getCommandStatus(task.taskId);
|
|
||||||
ctx.logger.info("command task", {
|
|
||||||
taskId: task.taskId,
|
|
||||||
state: status?.state,
|
|
||||||
exitCode: status?.exitCode
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`list`, `getStatus`, `attachOutput`, `cancel` and `retry` continue to operate on
|
|
||||||
tasks visible through IdeA's Work read model. `getCommandStatus` reads a launched
|
|
||||||
command task directly from the host task store.
|
|
||||||
|
|
||||||
### Toolchain Diagnostics
|
|
||||||
|
|
||||||
Use `ctx.services.tooling.diagnose()` to check external prerequisites without
|
|
||||||
hard-coding one stack into the SDK. A request can probe executables, inspect
|
|
||||||
environment variables and validate workspace files in one structured result.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import type { ActivateContext } from "@idea/plugin-sdk";
|
|
||||||
|
|
||||||
export async function activate(ctx: ActivateContext): Promise<void> {
|
|
||||||
const diagnostic = await ctx.services?.tooling.diagnose({
|
|
||||||
tools: [
|
|
||||||
{
|
|
||||||
id: "node",
|
|
||||||
executable: "node",
|
|
||||||
versionArgs: ["--version"],
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
env: [{ name: "PATH", required: true }],
|
|
||||||
files: [{ path: "package.json", kind: "file" }]
|
|
||||||
});
|
|
||||||
|
|
||||||
const node = diagnostic?.tools.find((tool) => tool.id === "node");
|
|
||||||
ctx.logger.info("tooling diagnostic", {
|
|
||||||
ok: diagnostic?.ok,
|
|
||||||
nodePresent: node?.present,
|
|
||||||
nodeVersion: node?.version,
|
|
||||||
messages: diagnostic?.messages
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The diagnostic API is intentionally generic: it does not install tools, does not
|
|
||||||
model Android devices or emulators, and does not expose language-specific ASTs.
|
|
||||||
|
|
||||||
### Events And Watch
|
|
||||||
|
|
||||||
Use `ctx.services.events.subscribe()` for stable public host/project events. The
|
|
||||||
runtime hides the host polling details and returns a disposable subscription.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import type { ActivateContext } from "@idea/plugin-sdk";
|
|
||||||
|
|
||||||
export async function activate(ctx: ActivateContext): Promise<void> {
|
|
||||||
const subscription = await ctx.services?.events.subscribe(
|
|
||||||
{
|
|
||||||
eventTypes: ["backgroundTaskChanged"],
|
|
||||||
capacity: 100,
|
|
||||||
onDropped: (count) => ctx.logger.warn("plugin events dropped", { count })
|
|
||||||
},
|
|
||||||
(event) => {
|
|
||||||
if (event.type === "backgroundTaskChanged") {
|
|
||||||
ctx.logger.info("task changed", {
|
|
||||||
taskId: event.taskId,
|
|
||||||
state: event.state
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (subscription) ctx.subscriptions.push(subscription);
|
|
||||||
|
|
||||||
const watch = await ctx.services?.workspace.watch("src", (event) => {
|
|
||||||
ctx.logger.info("workspace changed", {
|
|
||||||
path: event.path,
|
|
||||||
kind: event.kind,
|
|
||||||
operation: event.operation
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (watch) ctx.subscriptions.push(watch);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Public event retention is `bestEffortBounded`: events are retained per
|
|
||||||
subscription up to the requested/host-capped capacity, drained oldest-first, and
|
|
||||||
`onDropped` reports when older retained events were overwritten.
|
|
||||||
|
|
||||||
### Structured Config Documents
|
|
||||||
|
|
||||||
Use `ctx.services.config` when a plugin needs to read or update a structured
|
|
||||||
configuration file without reimplementing parsing and serialization.
|
|
||||||
|
|
||||||
First-lot format support is deliberately narrow:
|
|
||||||
|
|
||||||
- `json` only;
|
|
||||||
- inferred from `.json` when `format` is omitted;
|
|
||||||
- serialized as pretty JSON with a trailing newline;
|
|
||||||
- update modes: `mergePatch` and `replace`;
|
|
||||||
- `mergePatch` follows JSON merge-patch semantics: object keys are merged
|
|
||||||
recursively and `null` removes a key.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import type { ActivateContext } from "@idea/plugin-sdk";
|
|
||||||
|
|
||||||
export async function activate(ctx: ActivateContext): Promise<void> {
|
|
||||||
const config = await ctx.services?.config.readDocument({
|
|
||||||
path: ".ideai/hello-plugin.json"
|
|
||||||
});
|
|
||||||
|
|
||||||
await ctx.services?.config.updateDocument({
|
|
||||||
path: ".ideai/hello-plugin.json",
|
|
||||||
mode: "mergePatch",
|
|
||||||
value: {
|
|
||||||
enabled: true,
|
|
||||||
lastReadFormat: config?.format ?? "json"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
YAML, TOML, XML, `.properties` and stack-specific config models are not part of
|
|
||||||
this first lot.
|
|
||||||
|
|
||||||
Declare the additive `tooling` capability to receive `ctx.services` at runtime:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"capabilities": ["ui", "tooling"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Manifest Validation
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { validatePluginManifest } from "@idea/plugin-sdk";
|
|
||||||
|
|
||||||
const result = validatePluginManifest(manifestJson);
|
|
||||||
if (!result.success) {
|
|
||||||
console.error(result.errors);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This validator is deliberately strict for core fields and permissive about future unknown fields.
|
|
||||||
It is not a security boundary.
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
# Hello Plugin
|
|
||||||
|
|
||||||
Installable IdeA plugin example rebuilt from the public SDK types.
|
|
||||||
|
|
||||||
It exercises the current plugin primitives end to end:
|
|
||||||
|
|
||||||
- top-level menu: `Hello Plugin`;
|
|
||||||
- menu entry: `hello-plugin`;
|
|
||||||
- command: `hello-plugin`, returning `hello-world`;
|
|
||||||
- layout contribution: `hello-plugin.hello-world`, rendered as `hello-world`.
|
|
||||||
- tooling capability: logs the focused workspace project when `ctx.services` is available.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run typecheck:examples
|
|
||||||
npm run package:hello-plugin
|
|
||||||
```
|
|
||||||
|
|
||||||
The installable archive is emitted at `examples/hello-plugin/build/hello-plugin-0.1.0.zip`.
|
|
||||||
It contains `idea-plugin.json` at the ZIP root and the compiled ESM entrypoint at
|
|
||||||
`dist/index.js`, matching the manifest `main` field.
|
|
||||||
|
|
||||||
## Diagnostics
|
|
||||||
|
|
||||||
During activation the plugin logs:
|
|
||||||
|
|
||||||
- whether the command and layout runtime registries are available;
|
|
||||||
- successful registration of the `hello-plugin` command;
|
|
||||||
- successful registration of the `hello-plugin.hello-world` layout;
|
|
||||||
- availability of the workspace service from the `tooling` runtime capability;
|
|
||||||
- the first layout render, including project/node identifiers.
|
|
||||||
|
|
||||||
These messages are intentionally small and stable so installation, bundle import, activation and
|
|
||||||
layout rendering failures can be separated quickly in IdeA logs/devtools.
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
{
|
|
||||||
"ideaPluginManifestVersion": 1,
|
|
||||||
"id": "com.example.hello-plugin",
|
|
||||||
"displayName": "Hello Plugin",
|
|
||||||
"publisher": "IdeA Examples",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "SDK example plugin for validating command, menu and layout loading.",
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"engines": {
|
|
||||||
"idea": ">=0.1.0"
|
|
||||||
},
|
|
||||||
"trustLevel": "full",
|
|
||||||
"capabilities": [
|
|
||||||
"ui",
|
|
||||||
"tooling"
|
|
||||||
],
|
|
||||||
"contributes": {
|
|
||||||
"menus": [
|
|
||||||
{
|
|
||||||
"id": "hello-plugin.menu",
|
|
||||||
"label": "Hello Plugin",
|
|
||||||
"topLevel": true,
|
|
||||||
"order": 100
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"menuItems": [
|
|
||||||
{
|
|
||||||
"id": "hello-plugin.command.item",
|
|
||||||
"targetMenuId": "hello-plugin.menu",
|
|
||||||
"label": "hello-plugin",
|
|
||||||
"command": "hello-plugin",
|
|
||||||
"order": 10
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"layouts": [
|
|
||||||
{
|
|
||||||
"type": "hello-plugin.hello-world",
|
|
||||||
"label": "hello-world",
|
|
||||||
"component": "hello-world",
|
|
||||||
"order": 10
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
27
sdk/IdeaSDK/examples/hello-plugin/package-lock.json
generated
27
sdk/IdeaSDK/examples/hello-plugin/package-lock.json
generated
@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "hello-plugin",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "hello-plugin",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@idea/plugin-sdk": "file:../.."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"../..": {
|
|
||||||
"name": "@idea/plugin-sdk",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"devDependencies": {
|
|
||||||
"typescript": "^5.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@idea/plugin-sdk": {
|
|
||||||
"resolved": "../..",
|
|
||||||
"link": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "hello-plugin",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"scripts": {
|
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@idea/plugin-sdk": "file:../.."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,186 +0,0 @@
|
|||||||
import type { ActivateContext, IdeAPluginModule, PluginLayoutProps } from "@idea/plugin-sdk";
|
|
||||||
|
|
||||||
const COMMAND_ID = "hello-plugin";
|
|
||||||
const LAYOUT_TYPE = "hello-plugin.hello-world";
|
|
||||||
|
|
||||||
let hasLoggedFirstLayoutRender = false;
|
|
||||||
|
|
||||||
function HelloWorldLayout(props: PluginLayoutProps): string {
|
|
||||||
if (!hasLoggedFirstLayoutRender) {
|
|
||||||
hasLoggedFirstLayoutRender = true;
|
|
||||||
console.info("[hello-plugin] layout first render", {
|
|
||||||
projectId: props.projectId,
|
|
||||||
nodeId: props.nodeId,
|
|
||||||
layoutType: props.layoutType,
|
|
||||||
hasState: props.state !== undefined
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return "hello-world";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function activate(ctx: ActivateContext): void {
|
|
||||||
ctx.logger.info("activating hello-plugin", {
|
|
||||||
pluginId: ctx.pluginId,
|
|
||||||
hasCommands: Boolean(ctx.commands),
|
|
||||||
hasLayouts: Boolean(ctx.layouts)
|
|
||||||
});
|
|
||||||
|
|
||||||
const commandDisposable = ctx.commands?.registerCommand(COMMAND_ID, () => {
|
|
||||||
ctx.logger.info("command executed", { commandId: COMMAND_ID });
|
|
||||||
return "hello-world";
|
|
||||||
});
|
|
||||||
|
|
||||||
if (commandDisposable) {
|
|
||||||
ctx.subscriptions.push(commandDisposable);
|
|
||||||
ctx.logger.info("command registered", { commandId: COMMAND_ID });
|
|
||||||
} else {
|
|
||||||
ctx.logger.warn("command registry unavailable", { commandId: COMMAND_ID });
|
|
||||||
}
|
|
||||||
|
|
||||||
const layoutDisposable = ctx.layouts?.register({
|
|
||||||
type: LAYOUT_TYPE,
|
|
||||||
component: HelloWorldLayout
|
|
||||||
});
|
|
||||||
|
|
||||||
if (layoutDisposable) {
|
|
||||||
ctx.subscriptions.push(layoutDisposable);
|
|
||||||
ctx.logger.info("layout registered", {
|
|
||||||
layoutType: LAYOUT_TYPE,
|
|
||||||
component: "hello-world"
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
ctx.logger.warn("layout registry unavailable", { layoutType: LAYOUT_TYPE });
|
|
||||||
}
|
|
||||||
|
|
||||||
void useWorkspaceSdk(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function useWorkspaceSdk(ctx: ActivateContext): Promise<void> {
|
|
||||||
const workspace = ctx.services?.workspace;
|
|
||||||
if (!workspace) return;
|
|
||||||
|
|
||||||
const project = await workspace.getCurrentProject();
|
|
||||||
if (!project) {
|
|
||||||
ctx.logger.info("workspace service available without a focused project");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fixturePath = ".ideai/hello-plugin.txt";
|
|
||||||
await workspace.writeTextFile(fixturePath, "hello from @idea/plugin-sdk\n", project.id);
|
|
||||||
const file = await workspace.readTextFile(fixturePath, project.id);
|
|
||||||
const stat = await workspace.stat(fixturePath, project.id);
|
|
||||||
const listing = await workspace.listDirectory(".ideai", project.id);
|
|
||||||
const structure = await workspace.queryStructure({
|
|
||||||
projectId: project.id,
|
|
||||||
maxDepth: 2,
|
|
||||||
maxEntries: 100
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.logger.info("workspace file round-trip complete", {
|
|
||||||
projectId: project.id,
|
|
||||||
path: file.path,
|
|
||||||
bytes: stat.len,
|
|
||||||
ideaiEntries: listing.entries.length,
|
|
||||||
conventions: structure.conventions.map((convention) => convention.id)
|
|
||||||
});
|
|
||||||
|
|
||||||
const diagnostic = await ctx.services?.tooling.diagnose({
|
|
||||||
projectId: project.id,
|
|
||||||
tools: [
|
|
||||||
{
|
|
||||||
id: "echo",
|
|
||||||
executable: "echo",
|
|
||||||
versionArgs: ["hello-plugin-toolcheck"],
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
env: [{ name: "PATH", required: true }],
|
|
||||||
files: [{ path: fixturePath, kind: "file" }]
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.logger.info("tooling diagnostic complete", {
|
|
||||||
ok: diagnostic?.ok,
|
|
||||||
echoVersion: diagnostic?.tools.find((tool) => tool.id === "echo")?.version,
|
|
||||||
messages: diagnostic?.messages
|
|
||||||
});
|
|
||||||
|
|
||||||
const configPath = ".ideai/hello-plugin.json";
|
|
||||||
await workspace.writeTextFile(
|
|
||||||
configPath,
|
|
||||||
JSON.stringify({ enabled: true, launches: 0 }, null, 2) + "\n",
|
|
||||||
project.id
|
|
||||||
);
|
|
||||||
const configDocument = await ctx.services?.config.readDocument({
|
|
||||||
projectId: project.id,
|
|
||||||
path: configPath
|
|
||||||
});
|
|
||||||
await ctx.services?.config.updateDocument({
|
|
||||||
projectId: project.id,
|
|
||||||
path: configPath,
|
|
||||||
mode: "mergePatch",
|
|
||||||
value: { lastFormat: configDocument?.format ?? "json", launches: 1 }
|
|
||||||
});
|
|
||||||
ctx.logger.info("config document updated", {
|
|
||||||
path: configDocument?.path,
|
|
||||||
format: configDocument?.format
|
|
||||||
});
|
|
||||||
|
|
||||||
const watch = await workspace.watch(".ideai", (event) => {
|
|
||||||
ctx.logger.info("workspace watch event", {
|
|
||||||
path: event.path,
|
|
||||||
kind: event.kind,
|
|
||||||
operation: event.operation
|
|
||||||
});
|
|
||||||
}, project.id);
|
|
||||||
ctx.subscriptions.push(watch);
|
|
||||||
|
|
||||||
const events = await ctx.services?.events.subscribe(
|
|
||||||
{
|
|
||||||
projectId: project.id,
|
|
||||||
eventTypes: ["backgroundTaskChanged"],
|
|
||||||
pollIntervalMs: 2000,
|
|
||||||
onDropped: (count) => ctx.logger.warn("plugin events dropped", { count })
|
|
||||||
},
|
|
||||||
(event) => {
|
|
||||||
if (event.type === "backgroundTaskChanged") {
|
|
||||||
ctx.logger.info("background task changed", {
|
|
||||||
taskId: event.taskId,
|
|
||||||
state: event.state
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (events) ctx.subscriptions.push(events);
|
|
||||||
|
|
||||||
const ownerAgentId = await ctx.storage?.get<string>("helloPlugin.ownerAgentId");
|
|
||||||
if (!ownerAgentId) {
|
|
||||||
ctx.logger.info("command task example skipped: no owner agent configured");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const task = await ctx.services?.tasks.runCommand({
|
|
||||||
projectId: project.id,
|
|
||||||
ownerAgentId,
|
|
||||||
label: "Hello plugin command",
|
|
||||||
command: "echo",
|
|
||||||
args: ["hello from @idea/plugin-sdk"],
|
|
||||||
cwd: ".",
|
|
||||||
recordOnly: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (task) {
|
|
||||||
const status = await ctx.services?.tasks.getCommandStatus(task.taskId);
|
|
||||||
ctx.logger.info("command task launched", {
|
|
||||||
taskId: task.taskId,
|
|
||||||
state: status?.state ?? task.state,
|
|
||||||
exitCode: status?.exitCode ?? task.exitCode
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const plugin: IdeAPluginModule = {
|
|
||||||
activate
|
|
||||||
};
|
|
||||||
|
|
||||||
export default plugin;
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"declaration": false,
|
|
||||||
"declarationMap": false,
|
|
||||||
"noEmit": false,
|
|
||||||
"outDir": "dist",
|
|
||||||
"rootDir": "src",
|
|
||||||
"sourceMap": false,
|
|
||||||
"paths": {
|
|
||||||
"@idea/plugin-sdk": [
|
|
||||||
"../../dist/index.d.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/**/*.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"declaration": false,
|
|
||||||
"declarationMap": false,
|
|
||||||
"noEmit": true,
|
|
||||||
"rootDir": "../..",
|
|
||||||
"paths": {
|
|
||||||
"@idea/plugin-sdk": [
|
|
||||||
"../../src/index.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/**/*.ts",
|
|
||||||
"../../src/**/*.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
30
sdk/IdeaSDK/package-lock.json
generated
30
sdk/IdeaSDK/package-lock.json
generated
@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@idea/plugin-sdk",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "@idea/plugin-sdk",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"devDependencies": {
|
|
||||||
"typescript": "^5.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/typescript": {
|
|
||||||
"version": "5.9.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"bin": {
|
|
||||||
"tsc": "bin/tsc",
|
|
||||||
"tsserver": "bin/tsserver"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.17"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@idea/plugin-sdk",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "Minimal public TypeScript SDK for IdeA plugins.",
|
|
||||||
"type": "module",
|
|
||||||
"main": "./dist/index.js",
|
|
||||||
"types": "./dist/index.d.ts",
|
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"types": "./dist/index.d.ts",
|
|
||||||
"import": "./dist/index.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist",
|
|
||||||
"README.md"
|
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc -p tsconfig.json",
|
|
||||||
"build:hello-plugin": "tsc -p examples/hello-plugin/tsconfig.build.json",
|
|
||||||
"package:hello-plugin": "npm run build && npm run build:hello-plugin && node scripts/package-hello-plugin.mjs",
|
|
||||||
"typecheck:examples": "tsc -p examples/hello-plugin/tsconfig.json --noEmit",
|
|
||||||
"check": "npm run build && npm run typecheck:examples && npm run package:hello-plugin"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"idea",
|
|
||||||
"plugins",
|
|
||||||
"sdk"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"devDependencies": {
|
|
||||||
"typescript": "^5.5.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,124 +0,0 @@
|
|||||||
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
||||||
import { dirname, join } from "node:path";
|
|
||||||
|
|
||||||
const pluginRoot = join(process.cwd(), "examples", "hello-plugin");
|
|
||||||
const manifestPath = join(pluginRoot, "idea-plugin.json");
|
|
||||||
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
||||||
|
|
||||||
const main = requireString(manifest, "main");
|
|
||||||
const version = requireString(manifest, "version");
|
|
||||||
const archivePath = join(pluginRoot, "build", `hello-plugin-${version}.zip`);
|
|
||||||
const archiveEntries = [
|
|
||||||
{ archivePath: "idea-plugin.json", sourcePath: manifestPath },
|
|
||||||
{ archivePath: main, sourcePath: join(pluginRoot, main) },
|
|
||||||
{ archivePath: "README.md", sourcePath: join(pluginRoot, "README.md") }
|
|
||||||
];
|
|
||||||
const DOS_TIME_MIDNIGHT = 0;
|
|
||||||
const DOS_DATE_1980_01_01 = 33;
|
|
||||||
const CRC32_TABLE = Array.from({ length: 256 }, (_, index) => {
|
|
||||||
let value = index;
|
|
||||||
|
|
||||||
for (let bit = 0; bit < 8; bit += 1) {
|
|
||||||
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value >>> 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
await rm(join(pluginRoot, "build"), { recursive: true, force: true });
|
|
||||||
await mkdir(dirname(archivePath), { recursive: true });
|
|
||||||
|
|
||||||
const files = [];
|
|
||||||
for (const entry of archiveEntries) {
|
|
||||||
if (entry.archivePath.startsWith("/") || entry.archivePath.includes("..")) {
|
|
||||||
throw new Error(`Refusing unsafe archive path: ${entry.archivePath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
files.push({
|
|
||||||
archivePath: entry.archivePath,
|
|
||||||
data: await readFile(entry.sourcePath)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await writeFile(archivePath, createZip(files));
|
|
||||||
console.log(`created ${archivePath}`);
|
|
||||||
|
|
||||||
function requireString(record, key) {
|
|
||||||
if (typeof record[key] !== "string" || record[key].trim().length === 0) {
|
|
||||||
throw new Error(`idea-plugin.json field "${key}" must be a non-empty string`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return record[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
function createZip(files) {
|
|
||||||
const localFileHeaders = [];
|
|
||||||
const centralDirectoryHeaders = [];
|
|
||||||
let offset = 0;
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const filename = Buffer.from(file.archivePath, "utf8");
|
|
||||||
const checksum = crc32(file.data);
|
|
||||||
const localFileHeader = Buffer.alloc(30);
|
|
||||||
|
|
||||||
localFileHeader.writeUInt32LE(0x04034b50, 0);
|
|
||||||
localFileHeader.writeUInt16LE(20, 4);
|
|
||||||
localFileHeader.writeUInt16LE(0x0800, 6);
|
|
||||||
localFileHeader.writeUInt16LE(0, 8);
|
|
||||||
localFileHeader.writeUInt16LE(DOS_TIME_MIDNIGHT, 10);
|
|
||||||
localFileHeader.writeUInt16LE(DOS_DATE_1980_01_01, 12);
|
|
||||||
localFileHeader.writeUInt32LE(checksum, 14);
|
|
||||||
localFileHeader.writeUInt32LE(file.data.length, 18);
|
|
||||||
localFileHeader.writeUInt32LE(file.data.length, 22);
|
|
||||||
localFileHeader.writeUInt16LE(filename.length, 26);
|
|
||||||
localFileHeader.writeUInt16LE(0, 28);
|
|
||||||
|
|
||||||
localFileHeaders.push(localFileHeader, filename, file.data);
|
|
||||||
|
|
||||||
const centralDirectoryHeader = Buffer.alloc(46);
|
|
||||||
centralDirectoryHeader.writeUInt32LE(0x02014b50, 0);
|
|
||||||
centralDirectoryHeader.writeUInt16LE(20, 4);
|
|
||||||
centralDirectoryHeader.writeUInt16LE(20, 6);
|
|
||||||
centralDirectoryHeader.writeUInt16LE(0x0800, 8);
|
|
||||||
centralDirectoryHeader.writeUInt16LE(0, 10);
|
|
||||||
centralDirectoryHeader.writeUInt16LE(DOS_TIME_MIDNIGHT, 12);
|
|
||||||
centralDirectoryHeader.writeUInt16LE(DOS_DATE_1980_01_01, 14);
|
|
||||||
centralDirectoryHeader.writeUInt32LE(checksum, 16);
|
|
||||||
centralDirectoryHeader.writeUInt32LE(file.data.length, 20);
|
|
||||||
centralDirectoryHeader.writeUInt32LE(file.data.length, 24);
|
|
||||||
centralDirectoryHeader.writeUInt16LE(filename.length, 28);
|
|
||||||
centralDirectoryHeader.writeUInt16LE(0, 30);
|
|
||||||
centralDirectoryHeader.writeUInt16LE(0, 32);
|
|
||||||
centralDirectoryHeader.writeUInt16LE(0, 34);
|
|
||||||
centralDirectoryHeader.writeUInt16LE(0, 36);
|
|
||||||
centralDirectoryHeader.writeUInt32LE(0, 38);
|
|
||||||
centralDirectoryHeader.writeUInt32LE(offset, 42);
|
|
||||||
|
|
||||||
centralDirectoryHeaders.push(centralDirectoryHeader, filename);
|
|
||||||
offset += localFileHeader.length + filename.length + file.data.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
const centralDirectory = Buffer.concat(centralDirectoryHeaders);
|
|
||||||
const endOfCentralDirectory = Buffer.alloc(22);
|
|
||||||
|
|
||||||
endOfCentralDirectory.writeUInt32LE(0x06054b50, 0);
|
|
||||||
endOfCentralDirectory.writeUInt16LE(0, 4);
|
|
||||||
endOfCentralDirectory.writeUInt16LE(0, 6);
|
|
||||||
endOfCentralDirectory.writeUInt16LE(files.length, 8);
|
|
||||||
endOfCentralDirectory.writeUInt16LE(files.length, 10);
|
|
||||||
endOfCentralDirectory.writeUInt32LE(centralDirectory.length, 12);
|
|
||||||
endOfCentralDirectory.writeUInt32LE(offset, 16);
|
|
||||||
endOfCentralDirectory.writeUInt16LE(0, 20);
|
|
||||||
|
|
||||||
return Buffer.concat([...localFileHeaders, centralDirectory, endOfCentralDirectory]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function crc32(data) {
|
|
||||||
let value = 0xffffffff;
|
|
||||||
|
|
||||||
for (const byte of data) {
|
|
||||||
value = (value >>> 8) ^ CRC32_TABLE[(value ^ byte) & 0xff];
|
|
||||||
}
|
|
||||||
|
|
||||||
return (value ^ 0xffffffff) >>> 0;
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export { isPluginManifest, assertPluginManifest, validatePluginManifest } from "./manifest.js";
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
export type {
|
|
||||||
IdeAPluginCapability,
|
|
||||||
IdeAPluginManifest,
|
|
||||||
IdeAPluginEngineConstraints,
|
|
||||||
IdeAPluginLayoutContribution,
|
|
||||||
IdeAPluginMcpServerContribution,
|
|
||||||
IdeAPluginMenuItemContribution,
|
|
||||||
IdeAPluginTopLevelMenuContribution
|
|
||||||
} from "./manifest.js";
|
|
||||||
export {
|
|
||||||
isPluginManifest,
|
|
||||||
assertPluginManifest,
|
|
||||||
validatePluginManifest
|
|
||||||
} from "./manifest.js";
|
|
||||||
export type {
|
|
||||||
ActivateContext,
|
|
||||||
BackgroundTaskChangedEvent,
|
|
||||||
CommandDisposable,
|
|
||||||
CommandHandler,
|
|
||||||
CommandRegistry,
|
|
||||||
CommandTaskStatus,
|
|
||||||
ConfigDocument,
|
|
||||||
ConfigDocumentFormat,
|
|
||||||
ConfigDocumentReadOptions,
|
|
||||||
ConfigDocumentService,
|
|
||||||
ConfigDocumentUpdateOptions,
|
|
||||||
ConfigDocumentWriteResult,
|
|
||||||
ConfigUpdateMode,
|
|
||||||
DiagnosticMessage,
|
|
||||||
EnvDiagnostic,
|
|
||||||
EnvRequirement,
|
|
||||||
EventHandler,
|
|
||||||
EventService,
|
|
||||||
EventSubscribeOptions,
|
|
||||||
EventSubscription,
|
|
||||||
FileDiagnostic,
|
|
||||||
FileRequirement,
|
|
||||||
BackgroundTaskOutputAttachment,
|
|
||||||
BackgroundTaskRetryResult,
|
|
||||||
BackgroundTaskService,
|
|
||||||
BackgroundTaskStatus,
|
|
||||||
IdeAPluginModule,
|
|
||||||
JsonValue,
|
|
||||||
LayoutRegistry,
|
|
||||||
PluginLogger,
|
|
||||||
PluginLayoutAvailability,
|
|
||||||
PluginLayoutComponent,
|
|
||||||
PluginLayoutDefinition,
|
|
||||||
PluginLayoutProps,
|
|
||||||
PluginLayoutRenderResult,
|
|
||||||
PluginLayoutState,
|
|
||||||
PluginServices,
|
|
||||||
PluginStorage,
|
|
||||||
ProjectConvention,
|
|
||||||
ProjectModule,
|
|
||||||
ProjectStructure,
|
|
||||||
ProjectStructureEntry,
|
|
||||||
ProjectStructureEntryKind,
|
|
||||||
PublicEvent,
|
|
||||||
PublicEventType,
|
|
||||||
RunCommandTaskOptions,
|
|
||||||
TerminalOpenOptions,
|
|
||||||
TerminalReattachOptions,
|
|
||||||
TerminalReattachResult,
|
|
||||||
TerminalService,
|
|
||||||
TerminalSession,
|
|
||||||
ToolchainDiagnostic,
|
|
||||||
ToolchainDiagnosticRequest,
|
|
||||||
ToolDiagnostic,
|
|
||||||
ToolingService,
|
|
||||||
ToolRequirement,
|
|
||||||
WorkspaceBinaryFile,
|
|
||||||
WorkspaceDirEntry,
|
|
||||||
WorkspaceDirectoryListing,
|
|
||||||
WorkspaceFileChangedEvent,
|
|
||||||
WorkspaceProject,
|
|
||||||
WorkspaceResolvedPath,
|
|
||||||
WorkspaceService,
|
|
||||||
WorkspaceStat,
|
|
||||||
WorkspaceStructureQuery,
|
|
||||||
WorkspaceTextFile,
|
|
||||||
WorkspaceWatch,
|
|
||||||
WorkspaceWatchEvent,
|
|
||||||
WorkspaceWatchHandler
|
|
||||||
} from "./runtime.js";
|
|
||||||
@ -1,172 +0,0 @@
|
|||||||
const PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9.-]*[a-z0-9]$/;
|
|
||||||
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
||||||
export function validatePluginManifest(input) {
|
|
||||||
const errors = [];
|
|
||||||
if (!isRecord(input)) {
|
|
||||||
return { success: false, errors: ["manifest must be an object"] };
|
|
||||||
}
|
|
||||||
if (input.ideaPluginManifestVersion !== 1) {
|
|
||||||
errors.push("ideaPluginManifestVersion must be 1");
|
|
||||||
}
|
|
||||||
requireString(input, "id", errors);
|
|
||||||
requireString(input, "displayName", errors);
|
|
||||||
requireString(input, "version", errors);
|
|
||||||
requireString(input, "main", errors);
|
|
||||||
if (input.trustLevel !== "full") {
|
|
||||||
errors.push("trustLevel must be full");
|
|
||||||
}
|
|
||||||
optionalString(input, "description", errors);
|
|
||||||
optionalString(input, "publisher", errors);
|
|
||||||
if (typeof input.id === "string" && !PLUGIN_ID_PATTERN.test(input.id)) {
|
|
||||||
errors.push("id must contain lowercase letters, digits, dots or dashes, and start/end with an alphanumeric character");
|
|
||||||
}
|
|
||||||
if (typeof input.version === "string" && !SEMVER_PATTERN.test(input.version)) {
|
|
||||||
errors.push("version must use semver syntax, for example 0.1.0");
|
|
||||||
}
|
|
||||||
validateEngines(input.engines, errors);
|
|
||||||
validateCapabilities(input.capabilities, errors);
|
|
||||||
validateContributes(input.contributes, errors);
|
|
||||||
if (errors.length > 0) {
|
|
||||||
return { success: false, errors };
|
|
||||||
}
|
|
||||||
return { success: true, data: input, errors: [] };
|
|
||||||
}
|
|
||||||
export function isPluginManifest(input) {
|
|
||||||
return validatePluginManifest(input).success;
|
|
||||||
}
|
|
||||||
export function assertPluginManifest(input) {
|
|
||||||
const result = validatePluginManifest(input);
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(`Invalid IdeA plugin manifest: ${result.errors.join("; ")}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function validateEngines(value, errors) {
|
|
||||||
if (value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isRecord(value)) {
|
|
||||||
errors.push("engines must be an object when provided");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
optionalString(value, "idea", errors, "engines.idea");
|
|
||||||
}
|
|
||||||
function validateCapabilities(value, errors) {
|
|
||||||
if (value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!Array.isArray(value)) {
|
|
||||||
errors.push("capabilities must be an array when provided");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
value.forEach((capability, index) => {
|
|
||||||
if (capability !== "ui" && capability !== "mcp" && capability !== "tooling") {
|
|
||||||
errors.push(`capabilities[${index}] must be "ui", "mcp" or "tooling"`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function validateContributes(value, errors) {
|
|
||||||
if (value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isRecord(value)) {
|
|
||||||
errors.push("contributes must be an object when provided");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
validateArray(value, "menus", errors, (menu, index) => {
|
|
||||||
requireString(menu, "id", errors, `contributes.menus[${index}].id`);
|
|
||||||
requireString(menu, "label", errors, `contributes.menus[${index}].label`);
|
|
||||||
if (menu.topLevel !== true) {
|
|
||||||
errors.push(`contributes.menus[${index}].topLevel must be true`);
|
|
||||||
}
|
|
||||||
optionalNumber(menu, "order", errors, `contributes.menus[${index}].order`);
|
|
||||||
optionalString(menu, "icon", errors, `contributes.menus[${index}].icon`);
|
|
||||||
});
|
|
||||||
validateArray(value, "menuItems", errors, (item, index) => {
|
|
||||||
requireString(item, "id", errors, `contributes.menuItems[${index}].id`);
|
|
||||||
requireString(item, "targetMenuId", errors, `contributes.menuItems[${index}].targetMenuId`);
|
|
||||||
requireString(item, "label", errors, `contributes.menuItems[${index}].label`);
|
|
||||||
requireString(item, "command", errors, `contributes.menuItems[${index}].command`);
|
|
||||||
optionalNumber(item, "order", errors, `contributes.menuItems[${index}].order`);
|
|
||||||
optionalString(item, "icon", errors, `contributes.menuItems[${index}].icon`);
|
|
||||||
optionalString(item, "when", errors, `contributes.menuItems[${index}].when`);
|
|
||||||
});
|
|
||||||
validateArray(value, "layouts", errors, (layout, index) => {
|
|
||||||
requireString(layout, "type", errors, `contributes.layouts[${index}].type`);
|
|
||||||
requireString(layout, "label", errors, `contributes.layouts[${index}].label`);
|
|
||||||
requireString(layout, "component", errors, `contributes.layouts[${index}].component`);
|
|
||||||
optionalNumber(layout, "order", errors, `contributes.layouts[${index}].order`);
|
|
||||||
optionalString(layout, "icon", errors, `contributes.layouts[${index}].icon`);
|
|
||||||
optionalString(layout, "when", errors, `contributes.layouts[${index}].when`);
|
|
||||||
});
|
|
||||||
validateArray(value, "mcpServers", errors, (server, index) => {
|
|
||||||
requireString(server, "id", errors, `contributes.mcpServers[${index}].id`);
|
|
||||||
requireString(server, "displayName", errors, `contributes.mcpServers[${index}].displayName`);
|
|
||||||
requireString(server, "command", errors, `contributes.mcpServers[${index}].command`);
|
|
||||||
if (server.transport !== "stdio") {
|
|
||||||
errors.push(`contributes.mcpServers[${index}].transport must be "stdio"`);
|
|
||||||
}
|
|
||||||
optionalStringArray(server, "args", errors, `contributes.mcpServers[${index}].args`);
|
|
||||||
optionalStringRecord(server, "env", errors, `contributes.mcpServers[${index}].env`);
|
|
||||||
optionalString(server, "cwd", errors, `contributes.mcpServers[${index}].cwd`);
|
|
||||||
optionalBoolean(server, "autoStart", errors, `contributes.mcpServers[${index}].autoStart`);
|
|
||||||
optionalBoolean(server, "allowAbsoluteCommand", errors, `contributes.mcpServers[${index}].allowAbsoluteCommand`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function validateArray(record, key, errors, validateItem) {
|
|
||||||
const value = record[key];
|
|
||||||
if (value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!Array.isArray(value)) {
|
|
||||||
errors.push(`contributes.${key} must be an array when provided`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
value.forEach((item, index) => {
|
|
||||||
if (!isRecord(item)) {
|
|
||||||
errors.push(`contributes.${key}[${index}] must be an object`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
validateItem(item, index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function requireString(record, key, errors, label = key) {
|
|
||||||
if (typeof record[key] !== "string" || record[key].trim().length === 0) {
|
|
||||||
errors.push(`${label} must be a non-empty string`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function optionalString(record, key, errors, label = key) {
|
|
||||||
if (record[key] !== undefined && typeof record[key] !== "string") {
|
|
||||||
errors.push(`${label} must be a string when provided`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function optionalNumber(record, key, errors, label = key) {
|
|
||||||
if (record[key] !== undefined && typeof record[key] !== "number") {
|
|
||||||
errors.push(`${label} must be a number when provided`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function optionalBoolean(record, key, errors, label = key) {
|
|
||||||
if (record[key] !== undefined && typeof record[key] !== "boolean") {
|
|
||||||
errors.push(`${label} must be a boolean when provided`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function optionalStringArray(record, key, errors, label = key) {
|
|
||||||
const value = record[key];
|
|
||||||
if (value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
||||||
errors.push(`${label} must be an array of strings when provided`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function optionalStringRecord(record, key, errors, label = key) {
|
|
||||||
const value = record[key];
|
|
||||||
if (value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isRecord(value) || Object.values(value).some((item) => typeof item !== "string")) {
|
|
||||||
errors.push(`${label} must be an object of strings when provided`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function isRecord(value) {
|
|
||||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
||||||
}
|
|
||||||
@ -1,309 +0,0 @@
|
|||||||
export interface IdeAPluginManifest {
|
|
||||||
ideaPluginManifestVersion: 1;
|
|
||||||
id: string;
|
|
||||||
displayName: string;
|
|
||||||
version: string;
|
|
||||||
main: string;
|
|
||||||
trustLevel: "full";
|
|
||||||
description?: string;
|
|
||||||
publisher?: string;
|
|
||||||
engines?: IdeAPluginEngineConstraints;
|
|
||||||
capabilities?: IdeAPluginCapability[];
|
|
||||||
contributes?: {
|
|
||||||
menus?: IdeAPluginTopLevelMenuContribution[];
|
|
||||||
menuItems?: IdeAPluginMenuItemContribution[];
|
|
||||||
layouts?: IdeAPluginLayoutContribution[];
|
|
||||||
mcpServers?: IdeAPluginMcpServerContribution[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type IdeAPluginCapability = "ui" | "mcp" | "tooling";
|
|
||||||
|
|
||||||
export interface IdeAPluginEngineConstraints {
|
|
||||||
idea?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IdeAPluginTopLevelMenuContribution {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
topLevel: true;
|
|
||||||
order?: number;
|
|
||||||
icon?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IdeAPluginMenuItemContribution {
|
|
||||||
id: string;
|
|
||||||
targetMenuId: string;
|
|
||||||
label: string;
|
|
||||||
command: string;
|
|
||||||
order?: number;
|
|
||||||
icon?: string;
|
|
||||||
when?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IdeAPluginLayoutContribution {
|
|
||||||
type: string;
|
|
||||||
label: string;
|
|
||||||
component: string;
|
|
||||||
order?: number;
|
|
||||||
icon?: string;
|
|
||||||
when?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IdeAPluginMcpServerContribution {
|
|
||||||
id: string;
|
|
||||||
displayName: string;
|
|
||||||
command: string;
|
|
||||||
args?: string[];
|
|
||||||
env?: Record<string, string>;
|
|
||||||
cwd?: string;
|
|
||||||
transport: "stdio";
|
|
||||||
autoStart?: boolean;
|
|
||||||
allowAbsoluteCommand?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PluginManifestValidationResult =
|
|
||||||
| { success: true; data: IdeAPluginManifest; errors: [] }
|
|
||||||
| { success: false; data?: undefined; errors: string[] };
|
|
||||||
|
|
||||||
const PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9.-]*[a-z0-9]$/;
|
|
||||||
const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
||||||
|
|
||||||
export function validatePluginManifest(input: unknown): PluginManifestValidationResult {
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
if (!isRecord(input)) {
|
|
||||||
return { success: false, errors: ["manifest must be an object"] };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.ideaPluginManifestVersion !== 1) {
|
|
||||||
errors.push("ideaPluginManifestVersion must be 1");
|
|
||||||
}
|
|
||||||
|
|
||||||
requireString(input, "id", errors);
|
|
||||||
requireString(input, "displayName", errors);
|
|
||||||
requireString(input, "version", errors);
|
|
||||||
requireString(input, "main", errors);
|
|
||||||
if (input.trustLevel !== "full") {
|
|
||||||
errors.push("trustLevel must be full");
|
|
||||||
}
|
|
||||||
optionalString(input, "description", errors);
|
|
||||||
optionalString(input, "publisher", errors);
|
|
||||||
|
|
||||||
if (typeof input.id === "string" && !PLUGIN_ID_PATTERN.test(input.id)) {
|
|
||||||
errors.push("id must contain lowercase letters, digits, dots or dashes, and start/end with an alphanumeric character");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof input.version === "string" && !SEMVER_PATTERN.test(input.version)) {
|
|
||||||
errors.push("version must use semver syntax, for example 0.1.0");
|
|
||||||
}
|
|
||||||
|
|
||||||
validateEngines(input.engines, errors);
|
|
||||||
validateCapabilities(input.capabilities, errors);
|
|
||||||
validateContributes(input.contributes, errors);
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
return { success: false, errors };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, data: input as unknown as IdeAPluginManifest, errors: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isPluginManifest(input: unknown): input is IdeAPluginManifest {
|
|
||||||
return validatePluginManifest(input).success;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function assertPluginManifest(input: unknown): asserts input is IdeAPluginManifest {
|
|
||||||
const result = validatePluginManifest(input);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(`Invalid IdeA plugin manifest: ${result.errors.join("; ")}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateEngines(value: unknown, errors: string[]): void {
|
|
||||||
if (value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isRecord(value)) {
|
|
||||||
errors.push("engines must be an object when provided");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
optionalString(value, "idea", errors, "engines.idea");
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateCapabilities(value: unknown, errors: string[]): void {
|
|
||||||
if (value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(value)) {
|
|
||||||
errors.push("capabilities must be an array when provided");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
value.forEach((capability, index) => {
|
|
||||||
if (capability !== "ui" && capability !== "mcp" && capability !== "tooling") {
|
|
||||||
errors.push(`capabilities[${index}] must be "ui", "mcp" or "tooling"`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateContributes(value: unknown, errors: string[]): void {
|
|
||||||
if (value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isRecord(value)) {
|
|
||||||
errors.push("contributes must be an object when provided");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
validateArray(value, "menus", errors, (menu, index) => {
|
|
||||||
requireString(menu, "id", errors, `contributes.menus[${index}].id`);
|
|
||||||
requireString(menu, "label", errors, `contributes.menus[${index}].label`);
|
|
||||||
if (menu.topLevel !== true) {
|
|
||||||
errors.push(`contributes.menus[${index}].topLevel must be true`);
|
|
||||||
}
|
|
||||||
optionalNumber(menu, "order", errors, `contributes.menus[${index}].order`);
|
|
||||||
optionalString(menu, "icon", errors, `contributes.menus[${index}].icon`);
|
|
||||||
});
|
|
||||||
validateArray(value, "menuItems", errors, (item, index) => {
|
|
||||||
requireString(item, "id", errors, `contributes.menuItems[${index}].id`);
|
|
||||||
requireString(item, "targetMenuId", errors, `contributes.menuItems[${index}].targetMenuId`);
|
|
||||||
requireString(item, "label", errors, `contributes.menuItems[${index}].label`);
|
|
||||||
requireString(item, "command", errors, `contributes.menuItems[${index}].command`);
|
|
||||||
optionalNumber(item, "order", errors, `contributes.menuItems[${index}].order`);
|
|
||||||
optionalString(item, "icon", errors, `contributes.menuItems[${index}].icon`);
|
|
||||||
optionalString(item, "when", errors, `contributes.menuItems[${index}].when`);
|
|
||||||
});
|
|
||||||
validateArray(value, "layouts", errors, (layout, index) => {
|
|
||||||
requireString(layout, "type", errors, `contributes.layouts[${index}].type`);
|
|
||||||
requireString(layout, "label", errors, `contributes.layouts[${index}].label`);
|
|
||||||
requireString(layout, "component", errors, `contributes.layouts[${index}].component`);
|
|
||||||
optionalNumber(layout, "order", errors, `contributes.layouts[${index}].order`);
|
|
||||||
optionalString(layout, "icon", errors, `contributes.layouts[${index}].icon`);
|
|
||||||
optionalString(layout, "when", errors, `contributes.layouts[${index}].when`);
|
|
||||||
});
|
|
||||||
validateArray(value, "mcpServers", errors, (server, index) => {
|
|
||||||
requireString(server, "id", errors, `contributes.mcpServers[${index}].id`);
|
|
||||||
requireString(server, "displayName", errors, `contributes.mcpServers[${index}].displayName`);
|
|
||||||
requireString(server, "command", errors, `contributes.mcpServers[${index}].command`);
|
|
||||||
if (server.transport !== "stdio") {
|
|
||||||
errors.push(`contributes.mcpServers[${index}].transport must be "stdio"`);
|
|
||||||
}
|
|
||||||
optionalStringArray(server, "args", errors, `contributes.mcpServers[${index}].args`);
|
|
||||||
optionalStringRecord(server, "env", errors, `contributes.mcpServers[${index}].env`);
|
|
||||||
optionalString(server, "cwd", errors, `contributes.mcpServers[${index}].cwd`);
|
|
||||||
optionalBoolean(server, "autoStart", errors, `contributes.mcpServers[${index}].autoStart`);
|
|
||||||
optionalBoolean(server, "allowAbsoluteCommand", errors, `contributes.mcpServers[${index}].allowAbsoluteCommand`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateArray(
|
|
||||||
record: Record<string, unknown>,
|
|
||||||
key: string,
|
|
||||||
errors: string[],
|
|
||||||
validateItem: (item: Record<string, unknown>, index: number) => void
|
|
||||||
): void {
|
|
||||||
const value = record[key];
|
|
||||||
if (value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(value)) {
|
|
||||||
errors.push(`contributes.${key} must be an array when provided`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
value.forEach((item, index) => {
|
|
||||||
if (!isRecord(item)) {
|
|
||||||
errors.push(`contributes.${key}[${index}] must be an object`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
validateItem(item, index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function requireString(
|
|
||||||
record: Record<string, unknown>,
|
|
||||||
key: string,
|
|
||||||
errors: string[],
|
|
||||||
label = key
|
|
||||||
): void {
|
|
||||||
if (typeof record[key] !== "string" || record[key].trim().length === 0) {
|
|
||||||
errors.push(`${label} must be a non-empty string`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function optionalString(
|
|
||||||
record: Record<string, unknown>,
|
|
||||||
key: string,
|
|
||||||
errors: string[],
|
|
||||||
label = key
|
|
||||||
): void {
|
|
||||||
if (record[key] !== undefined && typeof record[key] !== "string") {
|
|
||||||
errors.push(`${label} must be a string when provided`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function optionalNumber(
|
|
||||||
record: Record<string, unknown>,
|
|
||||||
key: string,
|
|
||||||
errors: string[],
|
|
||||||
label = key
|
|
||||||
): void {
|
|
||||||
if (record[key] !== undefined && typeof record[key] !== "number") {
|
|
||||||
errors.push(`${label} must be a number when provided`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function optionalBoolean(
|
|
||||||
record: Record<string, unknown>,
|
|
||||||
key: string,
|
|
||||||
errors: string[],
|
|
||||||
label = key
|
|
||||||
): void {
|
|
||||||
if (record[key] !== undefined && typeof record[key] !== "boolean") {
|
|
||||||
errors.push(`${label} must be a boolean when provided`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function optionalStringArray(
|
|
||||||
record: Record<string, unknown>,
|
|
||||||
key: string,
|
|
||||||
errors: string[],
|
|
||||||
label = key
|
|
||||||
): void {
|
|
||||||
const value = record[key];
|
|
||||||
if (value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
||||||
errors.push(`${label} must be an array of strings when provided`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function optionalStringRecord(
|
|
||||||
record: Record<string, unknown>,
|
|
||||||
key: string,
|
|
||||||
errors: string[],
|
|
||||||
label = key
|
|
||||||
): void {
|
|
||||||
const value = record[key];
|
|
||||||
if (value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isRecord(value) || Object.values(value).some((item) => typeof item !== "string")) {
|
|
||||||
errors.push(`${label} must be an object of strings when provided`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
||||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export {};
|
|
||||||
@ -1,532 +0,0 @@
|
|||||||
export interface ActivateContext {
|
|
||||||
pluginId: string;
|
|
||||||
logger: PluginLogger;
|
|
||||||
subscriptions: CommandDisposable[];
|
|
||||||
commands?: CommandRegistry;
|
|
||||||
layouts?: LayoutRegistry;
|
|
||||||
storage?: PluginStorage;
|
|
||||||
/**
|
|
||||||
* Stable public service facade for plugins that need workspace, background
|
|
||||||
* task, or terminal operations. This intentionally does not expose IdeA's
|
|
||||||
* internal runtime/gateway objects.
|
|
||||||
*/
|
|
||||||
services?: PluginServices;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IdeAPluginModule {
|
|
||||||
activate(ctx: ActivateContext): void | Promise<void>;
|
|
||||||
deactivate?(): void | Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PluginLogger {
|
|
||||||
debug(message: string, ...args: unknown[]): void;
|
|
||||||
info(message: string, ...args: unknown[]): void;
|
|
||||||
warn(message: string, ...args: unknown[]): void;
|
|
||||||
error(message: string, ...args: unknown[]): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CommandHandler = (...args: unknown[]) => unknown | Promise<unknown>;
|
|
||||||
|
|
||||||
export interface CommandRegistry {
|
|
||||||
registerCommand(commandId: string, handler: CommandHandler): CommandDisposable;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CommandDisposable {
|
|
||||||
dispose(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PluginStorage {
|
|
||||||
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
||||||
set<T = unknown>(key: string, value: T): Promise<void>;
|
|
||||||
delete(key: string): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PluginLayoutState = JsonValue | undefined;
|
|
||||||
export type PluginLayoutAvailability = "available";
|
|
||||||
export type PluginLayoutRenderResult = unknown;
|
|
||||||
|
|
||||||
export interface PluginLayoutProps<TState extends PluginLayoutState = PluginLayoutState> {
|
|
||||||
/** Project currently hosting this layout cell. */
|
|
||||||
projectId: string;
|
|
||||||
/** Stable layout node id for this cell instance. */
|
|
||||||
nodeId: string;
|
|
||||||
/** Layout contribution type declared in `idea-plugin.json`. */
|
|
||||||
layoutType: string;
|
|
||||||
/** Opaque JSON-serializable state persisted by the host for this cell. */
|
|
||||||
state: TState;
|
|
||||||
/** Replaces the opaque state for this cell. Values must be JSON-serializable. */
|
|
||||||
setState(next: TState): void;
|
|
||||||
/** Present layouts are only mounted when available; fallback UI is host-owned. */
|
|
||||||
availability: PluginLayoutAvailability;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PluginLayoutComponent<TState extends PluginLayoutState = PluginLayoutState> = (
|
|
||||||
props: PluginLayoutProps<TState>,
|
|
||||||
) => PluginLayoutRenderResult;
|
|
||||||
|
|
||||||
export interface PluginLayoutDefinition<TState extends PluginLayoutState = PluginLayoutState> {
|
|
||||||
/** Must match a layout `type` declared in this plugin's manifest. */
|
|
||||||
type: string;
|
|
||||||
component: PluginLayoutComponent<TState>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LayoutRegistry {
|
|
||||||
register<TState extends PluginLayoutState = PluginLayoutState>(
|
|
||||||
definition: PluginLayoutDefinition<TState>,
|
|
||||||
): CommandDisposable;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PluginServices {
|
|
||||||
workspace: WorkspaceService;
|
|
||||||
tasks: BackgroundTaskService;
|
|
||||||
tooling: ToolingService;
|
|
||||||
events: EventService;
|
|
||||||
config: ConfigDocumentService;
|
|
||||||
terminal: TerminalService;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceProject {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
root: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceService {
|
|
||||||
/** Returns the currently focused project, or null when no project is active. */
|
|
||||||
getCurrentProject(): Promise<WorkspaceProject | null>;
|
|
||||||
/** Returns the root path for the given project or for the current project. */
|
|
||||||
getProjectRoot(projectId?: string): Promise<string>;
|
|
||||||
/** Reads IdeA's shared project context for the given or current project. */
|
|
||||||
readProjectContext(projectId?: string): Promise<string>;
|
|
||||||
/** Updates IdeA's shared project context for the given or current project. */
|
|
||||||
updateProjectContext(content: string, projectId?: string): Promise<void>;
|
|
||||||
/**
|
|
||||||
* Resolves and normalizes a plugin-visible path under the project root.
|
|
||||||
* Rejects absolute paths, `..`, empty segments and other paths the host
|
|
||||||
* considers outside the workspace sandbox.
|
|
||||||
*/
|
|
||||||
resolvePath(path: string, projectId?: string): Promise<WorkspaceResolvedPath>;
|
|
||||||
/** Reads a UTF-8 text file under the project root. */
|
|
||||||
readTextFile(path: string, projectId?: string): Promise<WorkspaceTextFile>;
|
|
||||||
/** Reads raw bytes from a file under the project root. */
|
|
||||||
readBinaryFile(path: string, projectId?: string): Promise<WorkspaceBinaryFile>;
|
|
||||||
/** Writes UTF-8 text under the project root using the host's controlled write path. */
|
|
||||||
writeTextFile(path: string, content: string, projectId?: string): Promise<void>;
|
|
||||||
/** Writes raw bytes under the project root using the host's controlled write path. */
|
|
||||||
writeBinaryFile(path: string, bytes: Uint8Array, projectId?: string): Promise<void>;
|
|
||||||
/** Lists one directory under the project root. Defaults to the workspace root. */
|
|
||||||
listDirectory(path?: string, projectId?: string): Promise<WorkspaceDirectoryListing>;
|
|
||||||
/**
|
|
||||||
* Returns basic metadata. Missing paths resolve to `{ exists: false }`; invalid
|
|
||||||
* paths and permission errors reject.
|
|
||||||
*/
|
|
||||||
stat(path: string, projectId?: string): Promise<WorkspaceStat>;
|
|
||||||
/**
|
|
||||||
* Extension point for host file watching. The MVP SDK reserves the public
|
|
||||||
* shape; hosts may reject with a clear not-implemented error until #127 lands.
|
|
||||||
*/
|
|
||||||
watch(path: string, handler: WorkspaceWatchHandler, projectId?: string): Promise<WorkspaceWatch>;
|
|
||||||
/** Queries a bounded, generic project structure read model. */
|
|
||||||
queryStructure(query?: WorkspaceStructureQuery): Promise<ProjectStructure>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceResolvedPath {
|
|
||||||
projectId: string;
|
|
||||||
root: string;
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceTextFile {
|
|
||||||
path: string;
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceBinaryFile {
|
|
||||||
path: string;
|
|
||||||
bytes: Uint8Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceDirEntry {
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
isDir: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceDirectoryListing {
|
|
||||||
path: string;
|
|
||||||
entries: WorkspaceDirEntry[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceStat {
|
|
||||||
path: string;
|
|
||||||
exists: boolean;
|
|
||||||
isFile: boolean;
|
|
||||||
isDir: boolean;
|
|
||||||
len: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceWatchEvent {
|
|
||||||
path: string;
|
|
||||||
kind: "created" | "modified" | "deleted" | "renamed" | "unknown";
|
|
||||||
operation: string;
|
|
||||||
projectId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WorkspaceWatchHandler = (event: WorkspaceWatchEvent) => void;
|
|
||||||
|
|
||||||
export interface WorkspaceWatch {
|
|
||||||
dispose(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceStructureQuery {
|
|
||||||
projectId?: string;
|
|
||||||
path?: string;
|
|
||||||
maxDepth?: number;
|
|
||||||
maxEntries?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ProjectStructureEntryKind = "file" | "directory";
|
|
||||||
|
|
||||||
export interface ProjectStructureEntry {
|
|
||||||
path: string;
|
|
||||||
name: string;
|
|
||||||
kind: ProjectStructureEntryKind;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProjectConvention {
|
|
||||||
id: string;
|
|
||||||
markerPath: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProjectModule {
|
|
||||||
path: string;
|
|
||||||
markerPath: string;
|
|
||||||
conventionId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProjectStructure {
|
|
||||||
projectId: string;
|
|
||||||
rootPath: string;
|
|
||||||
entries: ProjectStructureEntry[];
|
|
||||||
conventions: ProjectConvention[];
|
|
||||||
modules: ProjectModule[];
|
|
||||||
truncated: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BackgroundTaskStatus {
|
|
||||||
taskId: string;
|
|
||||||
ownerAgentId: string;
|
|
||||||
projectId: string;
|
|
||||||
kind: string;
|
|
||||||
status: "pending" | "running" | "completed" | "failed" | "cancelled" | "delivered";
|
|
||||||
exitCode: number | null;
|
|
||||||
summary: string | null;
|
|
||||||
stdoutTail: string | null;
|
|
||||||
stderrTail: string | null;
|
|
||||||
updatedAtMs: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BackgroundTaskOutputAttachment {
|
|
||||||
taskId: string;
|
|
||||||
scrollback: Uint8Array;
|
|
||||||
live: boolean;
|
|
||||||
detach(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BackgroundTaskRetryResult {
|
|
||||||
/** Present when the host reports the replacement task id. */
|
|
||||||
taskId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RunCommandTaskOptions {
|
|
||||||
/** Project that owns the command workspace. Defaults to the focused project. */
|
|
||||||
projectId?: string;
|
|
||||||
/** Agent id used by IdeA Work for ownership, cancellation and completion delivery. */
|
|
||||||
ownerAgentId: string;
|
|
||||||
/** Human-facing label shown in Work. Defaults to the command line. */
|
|
||||||
label?: string;
|
|
||||||
/** Executable to run. Arguments are passed separately, without shell parsing. */
|
|
||||||
command: string;
|
|
||||||
/** Arguments passed to the executable. */
|
|
||||||
args?: string[];
|
|
||||||
/** Relative working directory under the project root. Defaults to the root. */
|
|
||||||
cwd?: string;
|
|
||||||
/** Extra environment variables for the command. */
|
|
||||||
env?: Record<string, string> | Array<[string, string]>;
|
|
||||||
/** When true, completion is recorded without waking the owner agent. */
|
|
||||||
recordOnly?: boolean;
|
|
||||||
/** Optional absolute deadline, epoch milliseconds. */
|
|
||||||
deadlineMs?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CommandTaskStatus {
|
|
||||||
taskId: string;
|
|
||||||
ownerAgentId: string;
|
|
||||||
projectId: string;
|
|
||||||
kind: string;
|
|
||||||
state: "queued" | "running" | "waiting" | "completed" | "failed" | "cancelled" | "expired";
|
|
||||||
exitCode: number | null;
|
|
||||||
summary: string | null;
|
|
||||||
stdoutTail: string | null;
|
|
||||||
stderrTail: string | null;
|
|
||||||
createdAtMs: number;
|
|
||||||
updatedAtMs: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToolRequirement {
|
|
||||||
/** Stable id chosen by the plugin for this executable prerequisite. */
|
|
||||||
id: string;
|
|
||||||
/** Executable name or path to probe. */
|
|
||||||
executable: string;
|
|
||||||
/** Version/diagnostic arguments. Defaults host-side to `--version`. */
|
|
||||||
versionArgs?: string[];
|
|
||||||
/** Whether this tool must pass for the whole diagnostic to be ok. */
|
|
||||||
required?: boolean;
|
|
||||||
/** Extra environment variables for this probe. */
|
|
||||||
env?: Record<string, string> | Array<[string, string]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EnvRequirement {
|
|
||||||
/** Environment variable name. */
|
|
||||||
name: string;
|
|
||||||
/** Whether the variable must be present and match. */
|
|
||||||
required?: boolean;
|
|
||||||
/** Optional exact expected value. */
|
|
||||||
equals?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileRequirement {
|
|
||||||
/** Relative workspace path. */
|
|
||||||
path: string;
|
|
||||||
/** Whether the path must exist and match `kind`. */
|
|
||||||
required?: boolean;
|
|
||||||
/** Expected workspace path kind. */
|
|
||||||
kind?: "file" | "directory" | "any";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToolchainDiagnosticRequest {
|
|
||||||
/** Project to inspect. Defaults to the focused project. */
|
|
||||||
projectId?: string;
|
|
||||||
/** Relative working directory under the project root. Defaults to the root. */
|
|
||||||
cwd?: string;
|
|
||||||
/** Executable probes to run. */
|
|
||||||
tools?: ToolRequirement[];
|
|
||||||
/** Environment variable prerequisites to inspect. */
|
|
||||||
env?: EnvRequirement[];
|
|
||||||
/** Workspace file prerequisites to validate. */
|
|
||||||
files?: FileRequirement[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToolchainDiagnostic {
|
|
||||||
projectId: string;
|
|
||||||
cwd: string;
|
|
||||||
ok: boolean;
|
|
||||||
tools: ToolDiagnostic[];
|
|
||||||
env: EnvDiagnostic[];
|
|
||||||
files: FileDiagnostic[];
|
|
||||||
messages: DiagnosticMessage[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToolDiagnostic {
|
|
||||||
id: string;
|
|
||||||
executable: string;
|
|
||||||
present: boolean;
|
|
||||||
ok: boolean;
|
|
||||||
status: "ok" | "failed" | "missing";
|
|
||||||
required: boolean;
|
|
||||||
exitCode: number | null;
|
|
||||||
version: string | null;
|
|
||||||
stdout: string | null;
|
|
||||||
stderr: string | null;
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EnvDiagnostic {
|
|
||||||
name: string;
|
|
||||||
present: boolean;
|
|
||||||
ok: boolean;
|
|
||||||
required: boolean;
|
|
||||||
value: string | null;
|
|
||||||
status: "ok" | "missing" | "mismatch";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileDiagnostic {
|
|
||||||
path: string;
|
|
||||||
exists: boolean;
|
|
||||||
ok: boolean;
|
|
||||||
required: boolean;
|
|
||||||
kind: "file" | "directory" | "other" | "missing";
|
|
||||||
expectedKind: "file" | "directory" | "any" | null;
|
|
||||||
len: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DiagnosticMessage {
|
|
||||||
level: "info" | "warning" | "error";
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToolingService {
|
|
||||||
/** Runs generic external-toolchain diagnostics for executables, env and files. */
|
|
||||||
diagnose(request: ToolchainDiagnosticRequest): Promise<ToolchainDiagnostic>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PublicEventType = "workspaceFileChanged" | "backgroundTaskChanged";
|
|
||||||
|
|
||||||
export type PublicEvent = WorkspaceFileChangedEvent | BackgroundTaskChangedEvent;
|
|
||||||
|
|
||||||
export interface WorkspaceFileChangedEvent {
|
|
||||||
type: "workspaceFileChanged";
|
|
||||||
sequence: number;
|
|
||||||
occurredAtMs: number;
|
|
||||||
projectId: string;
|
|
||||||
path: string;
|
|
||||||
operation: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BackgroundTaskChangedEvent {
|
|
||||||
type: "backgroundTaskChanged";
|
|
||||||
sequence: number;
|
|
||||||
occurredAtMs: number;
|
|
||||||
projectId: string;
|
|
||||||
taskId: string;
|
|
||||||
ownerAgentId: string;
|
|
||||||
state: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EventSubscribeOptions {
|
|
||||||
/** Project to observe. Defaults to the focused project. */
|
|
||||||
projectId?: string;
|
|
||||||
/** Public event types to retain. Empty/omitted means every supported event. */
|
|
||||||
eventTypes?: PublicEventType[];
|
|
||||||
/** Per-subscription retained capacity. Host clamps to its supported bounds. */
|
|
||||||
capacity?: number;
|
|
||||||
/** Polling cadence used by the runtime facade. Defaults to 1000 ms. */
|
|
||||||
pollIntervalMs?: number;
|
|
||||||
/** Maximum events drained per poll. Host clamps to its supported bounds. */
|
|
||||||
maxEventsPerPoll?: number;
|
|
||||||
/** Called when the host reports dropped retained events for this subscription. */
|
|
||||||
onDropped?: (count: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EventSubscription {
|
|
||||||
readonly subscriptionId: string;
|
|
||||||
readonly projectId: string;
|
|
||||||
readonly eventTypes: PublicEventType[];
|
|
||||||
readonly retention: string;
|
|
||||||
dispose(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EventHandler = (event: PublicEvent) => void;
|
|
||||||
|
|
||||||
export interface EventService {
|
|
||||||
/** Subscribes to stable, best-effort bounded public host/project events. */
|
|
||||||
subscribe(options: EventSubscribeOptions, handler: EventHandler): Promise<EventSubscription>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type JsonValue =
|
|
||||||
| null
|
|
||||||
| boolean
|
|
||||||
| number
|
|
||||||
| string
|
|
||||||
| JsonValue[]
|
|
||||||
| { [key: string]: JsonValue };
|
|
||||||
|
|
||||||
export type ConfigDocumentFormat = "json";
|
|
||||||
export type ConfigUpdateMode = "mergePatch" | "replace";
|
|
||||||
|
|
||||||
export interface ConfigDocumentReadOptions {
|
|
||||||
/** Project that owns the config document. Defaults to the focused project. */
|
|
||||||
projectId?: string;
|
|
||||||
/** Relative path under the project root. */
|
|
||||||
path: string;
|
|
||||||
/** Explicit format. Omit to infer from extension. First lot supports only `json`. */
|
|
||||||
format?: ConfigDocumentFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConfigDocumentUpdateOptions extends ConfigDocumentReadOptions {
|
|
||||||
/** Update mode. Defaults host-side to `mergePatch`. */
|
|
||||||
mode?: ConfigUpdateMode;
|
|
||||||
/** Replacement value or JSON merge patch. */
|
|
||||||
value: JsonValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConfigDocument<T extends JsonValue = JsonValue> {
|
|
||||||
projectId: string;
|
|
||||||
path: string;
|
|
||||||
format: ConfigDocumentFormat;
|
|
||||||
value: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConfigDocumentWriteResult {
|
|
||||||
projectId: string;
|
|
||||||
path: string;
|
|
||||||
format: ConfigDocumentFormat;
|
|
||||||
mode: ConfigUpdateMode;
|
|
||||||
bytesWritten: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConfigDocumentService {
|
|
||||||
/** Reads and parses a structured config document. First lot supports JSON only. */
|
|
||||||
readDocument<T extends JsonValue = JsonValue>(
|
|
||||||
options: ConfigDocumentReadOptions,
|
|
||||||
): Promise<ConfigDocument<T>>;
|
|
||||||
/** Writes a full replacement or JSON merge patch. First lot supports JSON only. */
|
|
||||||
updateDocument(options: ConfigDocumentUpdateOptions): Promise<ConfigDocumentWriteResult>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BackgroundTaskService {
|
|
||||||
/** Launches a non-interactive command as a first-class IdeA background task. */
|
|
||||||
runCommand(options: RunCommandTaskOptions): Promise<CommandTaskStatus>;
|
|
||||||
/** Reads one command task directly from the host task store. */
|
|
||||||
getCommandStatus(taskId: string): Promise<CommandTaskStatus | null>;
|
|
||||||
/** Lists background tasks visible in the project work-state read model. */
|
|
||||||
list(projectId?: string): Promise<BackgroundTaskStatus[]>;
|
|
||||||
/** Reads one task status from the project work-state read model. */
|
|
||||||
getStatus(taskId: string, projectId?: string): Promise<BackgroundTaskStatus | null>;
|
|
||||||
/** Attaches to retained/live output for a task. */
|
|
||||||
attachOutput(
|
|
||||||
taskId: string,
|
|
||||||
onData: (bytes: Uint8Array) => void,
|
|
||||||
): Promise<BackgroundTaskOutputAttachment>;
|
|
||||||
/** Cancels a pending/running task. */
|
|
||||||
cancel(taskId: string): Promise<void>;
|
|
||||||
/** Retries a failed/cancelled task; future hosts may return the new task id. */
|
|
||||||
retry(taskId: string): Promise<BackgroundTaskRetryResult>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TerminalOpenOptions {
|
|
||||||
cwd?: string;
|
|
||||||
rows?: number;
|
|
||||||
cols?: number;
|
|
||||||
onData?: (bytes: Uint8Array) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TerminalReattachOptions {
|
|
||||||
onData?: (bytes: Uint8Array) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TerminalSession {
|
|
||||||
readonly sessionId: string;
|
|
||||||
write(data: Uint8Array): Promise<void>;
|
|
||||||
resize(rows: number, cols: number): Promise<void>;
|
|
||||||
detach(): void;
|
|
||||||
close(): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TerminalReattachResult {
|
|
||||||
session: TerminalSession;
|
|
||||||
scrollback: Uint8Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TerminalService {
|
|
||||||
/**
|
|
||||||
* Opens a shell PTY in the requested/current project directory. This MVP is a
|
|
||||||
* terminal control surface, not a command runner; use tasks for build/test
|
|
||||||
* commands that should be tracked in the Work panel.
|
|
||||||
*/
|
|
||||||
open(options?: TerminalOpenOptions): Promise<TerminalSession>;
|
|
||||||
/** Reattaches to an already-running PTY and returns retained scrollback. */
|
|
||||||
reattach(sessionId: string, options?: TerminalReattachOptions): Promise<TerminalReattachResult>;
|
|
||||||
/** Kills a PTY by id. */
|
|
||||||
close(sessionId: string): Promise<void>;
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2022",
|
|
||||||
"module": "NodeNext",
|
|
||||||
"moduleResolution": "NodeNext",
|
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"outDir": "dist",
|
|
||||||
"rootDir": "src",
|
|
||||||
"strict": true,
|
|
||||||
"skipLibCheck": true
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/**/*.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user