JSConsole for Alfresco
Repository ·
Report an issue ·
Licence: Apache-2.0 ·
PleoSoft
Write, run and get IntelliSense for Alfresco repository JavaScript without leaving VS Code: write the
script in the editor, run it against a live repository, and get the output, the errors and the
completions back where you wrote it.
Execution goes through the JavaScript Console web scripts of
OOTBee Support Tools on the repository tier, so
there is nothing of ours running inside the repository and nothing to deploy beyond a module most
administrators already have. No Share, no custom repository module. Output streams while the script
runs, and a Rhino error comes back mapped to the line you wrote rather than the line the console
executed.

What the editor adds over that console:
- Completion and hover for the script API, generated per profile from the server the profile points
at, so an unfamiliar method is documented by the machine that is going to run it. Only what that
server's engine runs is offered.
- Diagnostics: syntax the engine will reject, root objects that do not exist, members an object does
not have, and prefixed names the content model does not know.
- Recipes: measured snippets for the common tasks, and your own, kept as ordinary scripts in folders
you choose.
- Five views: the scripts you declared, the recipes, the runs you have made, and the connected
server's script API and content model.
- Run parameters — transaction mode, run-as, space, document, arguments — set per run or pinned in
the file with a directive.
- A notebook kernel, for answering one question across several cells.
- A language server and an MCP server, so the same knowledge reaches another editor or an agent
working in the folder.

Read-write runs need explicit intent. The default is read-only.
Scope: administrative scripts. Migrations, bulk fixes, reports, repairs and investigations — the
scripts you run against a repository under an admin account and keep in your own Git repository. It is
not a web script development or testing tool: no descriptors, no FreeMarker, no HTTP request or
response, no deploy or refresh. For that keep using the browser, with the Support Tools pages in Share
and Alfresco's Web Scripts Home at /alfresco/s/index.
New here? If you want to use it, start with docs/USER-GUIDE.md: installing
it, connecting to a server, and what the editor does once both are in place. If you want to
change it, start with CONTRIBUTING.md, which covers the toolchain, the test setup
and the conventions. This README is the reference behind them.
- docs/USER-GUIDE.md — what the Alfresco script API is, connecting and
authenticating, TLS, syncing with your server, and moving your existing scripts into git.
- docs/HOWTO.md — short recipes with screenshots, one per task.
- docs/COMMANDS.md — every command, grouped by what you are trying to do.
- docs/RECIPES.md — every built-in recipe with its code and the traps it avoids, and
how to write your own.
- docs/SERVERS.md — the language server and the MCP server: how to start each, what
they read, what they answer, and how to wire them into something that is not VS Code.
Who makes it. PleoSoft, an Alfresco consultancy. The source, the issue
tracker and the releases are on GitHub.
Licence and requirements. Apache-2.0. Requires
Order of the Bee Support Tools on the
repository, which is a separate LGPL v3 module installed by your administrators. This extension calls
its JavaScript Console web scripts over HTTP and ships none of its code. Not affiliated with or
endorsed by Hyland or the Order of the Bee.
This is a pre-release. Please report anything that surprises you at
github.com/PleoSoft/jsconsole-for-alfresco/issues.
The JSConsole Log output channel carries the request trace, and Developer: Set Log Level... turns it
up; that log plus your Alfresco and OOTBee versions is usually the whole story. Behaviour of the console
itself that we have already measured is under Known upstream issues — worth a
glance before reporting one of those as ours.
Features
- Run in Alfresco (
Ctrl+Alt+R, the play button in the editor title, or the JSConsole for Alfresco
submenu on the editor's right-click, both shown only on Alfresco scripts). Runs the current file, or just
what you have selected. print() output streams into the JSConsole Output output channel while the
script runs.
- Notebooks. An
.alfnb file interleaves markdown with code cells that run on the active profile and
show their output inline, so an investigation becomes something you can save and hand over.
- Run history. Every run is kept locally with its script, parameters and output. Reopen the script,
rerun it, compare two runs side by side, or throw them away.
- Environment guardrails. A profile declares whether it points at development, test or production.
Production shows red in the status bar and makes you type the profile name before anything that can write,
or before running as another user. A production profile can also refuse writes outright.
- Script parameters like the Share console. Transaction mode (read-only, read-write, none) and run-as user
sit in the status bar; JSConsole: Script Parameters... also covers the
space and document root objects
(by nodeRef or by path below Company Home), URL arguments for args, and the dump limit. A file can pin its
own parameters with a header line such as // @jsconsole transaction=readwrite runas=alice. Read-write runs
ask for confirmation.
- Errors mapped back to your file. Rhino error lines are translated to the line in your script and shown
as a diagnostic in the editor, with the server stack trace in the output channel.
- Server profiles. Several servers, switch from the status bar. Passwords live in VS Code's secret storage.
- Typings generated from the live server. Refresh API Typings from Server — the download button on the
Script API and Content Model views, or the first entry when you click the profile in the status bar — asks the
repository for its script API, its content model and what its engine accepts. VS Code's TypeScript service
then gives you completion, hover and signature help for
search, companyhome and everything else, through a
bundled plugin, so nothing is written into your project. Definitions are cached per profile, so switching
servers switches the model too, and the views show which server they describe and how long ago it was read.
- A completion list that tells the truth about Rhino. It offers only the standard library the server's
engine has — no
Promise, so no .then() — only the members an object really has rather than every word in
the file, and getters such as getProperties() measured on your server. After a dot on a list or a map
that starts its line, it offers the loop that works there: an indexed for for an array, for…in for a map, where forEach fails.
- Members an object does not have, underlined.
task.then(...) or properties.forEach(...) is an error
before the run rather than a failure during it, for every object whose whole surface is known. Calling
or assigning such a member fails on the server and is an error; reading one only gives undefined, or
null on a property map, so that is a warning.
- The content model, browsable and checked. A view lists every type and aspect with its properties
and associations, grouped by namespace. A language server completes QNames inside string literals, knows
that
addAspect( wants an aspect and TYPE: wants a type, and warns when a name's namespace exists but
the name does not, which catches typos before a run.
- Syntax checked against the actual engine. The extension asks each server what its JavaScript engine
accepts, then underlines what it will reject. Writing
let against a repository whose Rhino only has
var is an error in the editor rather than a failed round trip.
- Search queries checked too, including the Lucene escaping rule that catches everyone. Typos in QNames,
root objects and query languages come with a "did you mean" quick fix.
- Recipes for the tasks that go wrong quietly. Nineteen of them — searching, versions, permissions, sites,
workflows, creating and deleting. Type
search, version or delete and press Ctrl+Space for a worked
script, with the reason it is written that way: a query without paging returns the server default and looks
complete, a property set before addAspect() is silently discarded, a caught repository error discards the
whole run. Every trap was measured on a real server. Add your own as ordinary .js files in a folder shared
with the team, and they appear beside the built-in ones in completion, in the Recipes view and for AI
assistants. They appear only in files recognised as Alfresco scripts, never in ordinary JavaScript. See
docs/RECIPES.md.
- An MCP server for AI assistants, bundled and registered automatically with VS Code, with one command to
wire it into Claude Code. Eight tools let an assistant read your repository's root objects, content model
and engine capabilities instead of recalling what Alfresco usually has, and check a script before showing it
to you. It runs locally, holds no credentials and can change nothing. See docs/SERVERS.md.
Requirements
- VS Code 1.101 or later.
- Alfresco Content Services with OOTBee Support Tools installed on the repository. Developed and tested
against Community 25.2.0 with OOTBee Support Tools 1.2.3.0; older versions of either are untested.
- A user that is a member of
ALFRESCO_ADMINISTRATORS (the console web scripts are admin-only).
- Authentication: Basic (alfrescoNtlm or LDAP chains) or the external subsystem (trusted header). Ticket and
OIDC/Keycloak modes are planned.
Running part of a file
Ctrl+Alt+R runs the whole file, or only the selection when there is one. JSConsole: Run Selection in Alfresco does
the same thing explicitly, and the JSConsole for Alfresco submenu on the right-click menu offers whichever applies.
Two details make a selection behave the way you would want:
- Error lines still point at the right place. A failure on the second line of a selection that starts at
line 6 is reported against line 7 of the file, not line 2.
@jsconsole header directives still apply, because they are read from the whole file. A script pinned
to readwrite stays read-write when you run three lines of it.
A selection is sent as a complete script, so it has to stand on its own. Selecting the body of a function
that reads variables declared above it will fail, exactly as it would in the Share console.
Which files count as Alfresco scripts
Everything this extension knows is true of repository scripts and false of ordinary JavaScript, so the
rules have to be scoped. Told to check a Node build script, the engine check would report let and the
root object check would report require, module and process.
Declare your folders. Right-click a folder in the Explorer and choose JSConsole for Alfresco ›
Declare Folder as Alfresco Scripts, or use the gear in the Scripts view. That writes a glob into jsconsole.scripts.include in
that workspace folder's settings, so the whole team inherits it. Remove one from the folder's own context
menu in the Scripts view, or through the gear.
The Scripts view lists declared folders only. Until you declare one it stays empty and offers to help,
rather than listing whatever JavaScript happens to be lying around and calling it yours. A declaration
also keeps the run commands in the editor menus for every file in the folder, however little it holds
yet, and decides which folders get definitions written for them.
Diagnostics judge each file on its own, declared or not, because silence would just look broken.
A file counts as an Alfresco script when it uses a distinctive root object such as
search or companyhome, reaches for the Rhino Java bridge through Packages or java.lang, calls a
bare print() or dump(), or carries an @jsconsole line. A file using require, module.exports,
import, window or the DOM is never checked, whatever else it contains.
Adding // @jsconsole to a file always marks it as yours, which is the way to be certain.
| Setting |
Default |
Description |
jsconsole.scripts.include |
[] |
Folders that hold Alfresco scripts. Empty means an empty Scripts view. Diagnostics judge each file either way. |
jsconsole.diagnostics.scope |
auto |
auto, marked for only @jsconsole files, or all. |
Declaring script folders
The Scripts view shows nothing until you say which folders hold Alfresco scripts. Right-click a folder
in the Explorer and choose JSConsole for Alfresco › Declare Folder as Alfresco Scripts, or run
JSConsole: Configure Script Folders. A declared folder can be undeclared from its own row.
A declaration is stored in jsconsole.scripts.include as a path relative to the workspace folder that
contains it, and written to that folder's settings. That matters in a multi-root workspace: written at
workspace level, samples/** would mean "samples under every root", and every root would look declared.
A declared folder is listed as soon as it exists, whether or not it holds a script yet, so you can see
the declaration was made and take it back. A declaration naming a folder that is not there is left out
rather than invented, which is what a stale entry or one meant for another root looks like.
Declaring a whole workspace folder gives it no folder row of its own, so when it matches nothing the row
that says so carries the remove command instead. Every declaration is removable from the view; none of
them needs the settings file edited by hand.
The extension has its own icon in the activity bar, holding five views.
- Scripts. The
.js and .alfnb files in the folders you have declared, as a folder tree that
mirrors the one on disk. Nothing is listed until a folder is declared. A chain of single subfolders
collapses into one row, following your own explorer.compactFolders setting. Each script has a run
button on the row, so you can run one without opening it. Dependencies and build output are skipped,
including target, which matters in an Alfresco SDK project where Maven copies every script into it.
- Recipes. Your own recipe folders, then the built-in recipes. Open, insert or copy one, and add,
create or remove a folder. See Your own recipes.
- Run History. Described below.
- Script API. Described below.
- Content Model. Described below.
A view lives in one container, so these five are not also in the Explorer. Drag any of them back there if
you prefer, and VS Code will remember.
Running a script you already have open is unchanged: the play button in the editor title bar, the
JSConsole for Alfresco submenu on the right-click menu, or Ctrl+Alt+R. Notebook cells keep their own run buttons.
| Setting |
Default |
Description |
jsconsole.scripts.include |
[] |
Folders and globs for the Scripts view, relative to each workspace folder. Empty means the view lists nothing. |
jsconsole.scripts.maxFiles |
500 |
Most files listed per workspace folder before the view asks you to narrow the search. |
Notebooks
JSConsole: New Notebook creates an .alfnb file: markdown cells for the reasoning, code cells that run
on the active profile with their output underneath. samples/investigation.alfnb is a worked example.
Each cell is sent to the server as its own script, so cells do not share variables. Anything a later
cell needs, it looks up again. That is a property of the console, not a limitation of the notebook.
The status bar under a code cell shows its transaction mode. Click it to pin readonly, readwrite or
none for that cell alone, which is how a notebook can stay read-only except for the one step that
writes. A // @jsconsole line inside the cell wins over that, and the environment guardrails apply to every
cell exactly as they do to a file.
Running a notebook runs its cells in order and stops at the first one that fails. Stored output is not
saved with the file, so committing an .alfnb shares the questions and the code, not the answers.
JSConsole: Export Notebook as Script flattens the whole notebook into one .js file, markdown included
as comments.
Running as another user
Set a run-as user from the status bar, from Run in Alfresco with Options..., or with an
// @jsconsole runas=alice header line. The connected user must be an administrator.
Two things about run-as are worth knowing, because neither is obvious and neither is our doing. Both are
listed under Known upstream issues below.
- Permissions follow the run-as user, but
person and userhome do not. Those root objects always
describe the user you connected as. A script that reports its own identity through person will report
the wrong one. AuthenticationUtil.getRunAsUser() returns the effective user.
- Alfresco accepts any name. A typo is not rejected; the script runs as a stranger holding only
GROUP_EVERYONE, which on a typical repository can still read most content, so the run looks fine and
tells you nothing. The extension therefore checks the name against the server the first time you use it
on a profile and asks before running if there is no such person. The check costs one read-only round
trip per name per profile, and if it cannot answer it never blocks the run.
samples/06-run-as-and-people.js demonstrates both.
Script API
The Script API view lists every root object the connected repository exposes, read from the same
download the typings come from, so it describes that server rather than a list that drifts. On ACS 25.2
with OOTBee 1.2.3.0 the download declares 56 root objects over 209 types: 39 from the repository payload
and 17 from the web script payload. The view lists 41 of them by default, for the reason below.
Three things the raw payload gets wrong for a reader are undone first:
- Overloads. The server sends
luceneSearch, luceneSearch2 ... luceneSearch6 as separate
members, each naming the real one. Listing them as written would invent five methods that do not
exist, so they become one entry with six signatures, fewest arguments first.
- Inherited members. These sit behind a prototype and are not repeated on the subtype.
utils has
21 members, 14 of them inherited, and would look nearly empty without following it. Inherited ones
are marked with the type they came from.
- Boxed Java types.
JavaString is shown as string.
A member whose value is a type the payload knows can be opened, so the tree walks from search to
ScriptNode to NodeProperties without anyone needing those names. Depth is capped, because the API
is full of cycles: a node has a parent, which is a node.
The view lists what a console script can use, in two groups: Root objects and Console functions.
It is not grouped by which payload file a global came from, because that describes the download rather
than anything you can act on.
The server also sends a web script payload. Those root objects do resolve in the console, since it runs
your script from inside a web script of its own, but they describe an HTTP request you are not serving:
url is the console's own endpoint, webscript its own descriptor, headers the headers of the request
that carried your script. This extension runs administrative scripts, not web scripts, so they are left
out.
Two exceptions, and one caveat:
args and argsM are always listed. The console supplies them from its URL arguments field, which is
what this extension's own arguments parameter feeds.
jsonUtils, stringUtils, roothome and server are in that payload too, and all four work. Set
jsconsole.scriptApi.includeWebScriptGlobals to true to list the whole payload.
Either way they stay in the generated typings and in the unknown-global check, so using one is completed
and never reported as a name the repository does not have.
| Action |
What it does |
| Search the Script API |
One list of every member of every object, for when you remember the method but not the object it hangs off |
| Filter |
Narrows the tree to matching object and member names |
| Insert at Cursor |
Writes the call with its parameter names as placeholders |
| Copy Signature |
Puts the full signature on the clipboard |
print and dump are listed under Console functions. They are injected by the console around your script
rather than registered by the repository, so they are not in the payload at all and come from a list
the typings generator shares, which is what keeps the two from disagreeing.
Custom root objects registered by your own modules appear here too, once the definitions have been
refreshed against that server. If the editor offers a name this view does not list, the likely cause is
that the definitions were last refreshed against a different profile than the one now active.
Content model
The Content Model view shows what the active repository actually defines: every type and
every aspect, grouped by namespace, each expanding to its properties and associations. Inherited members are
listed too and say which class they come from. Click any entry to insert its QName at the cursor, quoted
unless you are already inside a string. The filter in the view title narrows it to one namespace, which is
how you look at a custom model on its own.
The same data drives the language server:
- Completion knows the context. A string after
addAspect( or ASPECT: offers aspects; one after
TYPE: or in a createFile type argument offers types; anywhere else you get properties and associations.
- Typos are reported. A name such as
cm:naem, whose namespace the repository uses but whose name it
does not define, is flagged as a warning. A name in a namespace the repository does not have is left alone,
because that is usually a string that merely looks like a QName rather than a mistake.
It comes from GET /alfresco/s/api/classes, a stock Alfresco web script rather than an OOTBee one, so it
needs only an ordinary user account. On a stock repository that is about 1 MB, fetched in around 100 ms.
If the repository will not serve it, everything else still works and only these features stay off.
What the engine will actually accept
Alfresco repositories run Rhino, and which JavaScript it accepts is not something you can predict. The
local test server, ACS 25.2 on Rhino 1.7.14, gives this:
| Works |
Does not work |
const, arrow functions, template literals |
let, class, for...of |
| destructuring, getters and setters |
spread and rest, Promise, Map, Set, Symbol |
const works but let does not. Arrow functions work but class does not. Destructuring works but spread
does not. Nobody would guess that, and a version number will not tell you either, since Alfresco sets the
language version and two servers on the same Rhino can differ.
So the extension asks. A refresh runs a small read-only probe that tries each feature and reports what
survived, caches the answer with the profile's other definitions, and the language server then underlines
exactly what that server will reject, with the alternative to use. The same pass reports a reference to a
root object the profile does not register, which is how a script that works on development fails on a
production server that is missing an addon.
Both checks stay silent until a profile has been refreshed, because guessing would be worse than saying
nothing.
Search queries
search.query and search.luceneSearch calls are read statically, which catches two things that otherwise
only show up at run time.
The Lucene escaping rule. In a lucene query a property field is written @cm\:name:value: the first
colon belongs to the field name and has to be escaped. Unescaped, the query throws rather than quietly
returning nothing. The check reads the string the repository would receive rather than the source text, so
it also catches "@cm\:name:x" written with one backslash, which JavaScript silently eats before the
server ever sees it. The quick fix rewrites the whole literal.
Only lucene queries are checked. In fts-alfresco, cm:name:value needs no escaping, and a sort column
passed to luceneSearch is written @cm:name with no escape either, so neither is flagged.
The language name. search.query({ language: "fts" }) fails at run time; the repository accepts
fts-alfresco, lucene, cmis-strict, cmis-alfresco, xpath and db-afts. A near miss is reported
with the correction as a quick fix.
Where completion comes from
Three different things suggest code in a repository script, and only two of them are this extension.
| Source |
What it offers |
| VS Code's JavaScript service, given the generated declarations by the bundled plugin |
members after a dot, with real signatures and overloads, and the loops that work on a list or a map |
| This extension's language server |
prefixed names inside strings, and the diagnostics below |
| An AI assistant, if one is installed |
whole lines of grey ghost text, guessed rather than read |
The third knows nothing about your repository. It writes what an Alfresco call tends to look like, which
is often right and sometimes invents a parameter that does not exist. Accepting one of those is how a
call like this gets written:
search.selectNodes("/app:company_home//*", "cm:name, cm:created");
There is no property-list parameter. The two-argument form of selectNodes is (store, search), so the
second argument is run as the query and the server answers Failed to execute search: cm:name, cm:created.
That call is now reported before it runs. Press Ctrl+Space to see the real signatures, which come from
the server and list every overload it actually has.
Assign to a variable
A call whose result goes nowhere is how a script usually starts: run search.query(...), see what it
does, then want to keep the answer. Put the cursor on the call and the lightbulb offers Assign to
"results", which is Eclipse's Ctrl+2 L. Nothing in VS Code's JavaScript support does this.
The name comes from the call rather than a counter, so getPerson gives person, createFolder gives
folder, listSites gives sites and childByNamePath gives child. A search names its query and
never its result, so those give results. A name already in scope is not shadowed; the new one gets a
number instead.
It is offered only where it would be correct: the call has to be the whole statement. In
print(search.query({}).length) the cursor may sit on the search, but a declaration in front of that
line would name print, so nothing is offered.
Filling in arguments
Accepting a completion inserts the name alone, because js/ts.suggest.completeFunctionCalls is off
by default in VS Code. Run JSConsole: Fill Arguments When Completing a Function to turn it on, and
accepting selectNodes writes selectNodes(search) with the parameter selected. It is a command rather
than something the extension sets for you, because it applies to all JavaScript in the workspace: it
writes the setting under "[javascript]" in workspace settings, or in user settings when no folder is
open. On a VS Code from before the js/ts.* rename it writes javascript.suggest.completeFunctionCalls
instead.
Your own recipes
The Recipes view in the JSConsole sidebar lists the built-in recipes and any folders of your own. A recipe
of yours is an ordinary .js script with a /** ... */ header — @recipe for its id, then @title,
@task, @keywords and any number of @trap lines — so you write and run it like any other script, and
it is offered only once it parses. A recipe of yours with a built-in id replaces the built-in one. Create Recipe Folder... sets one up in the project with an example, Save Selection as
Recipe... turns code you have into one, and Copy to My Recipes... gives you a built-in one to change.
They are offered in completion, labelled with their folder, and served to agents through the MCP server.
The folders are listed in jsconsole.recipes.folders. Share one with the team through workspace
settings, or keep one for yourself in user settings; both apply at once. jsconsole.recipes.enabled
turns recipe completion off.
Inline suggestions
The grey text proposed as you type comes from GitHub Copilot or another provider, not from this
extension, and it is written from general JavaScript habit without reading the typings. In a repository
script that means calls like task.then(...) or properties.forEach(...), which the server rejects. The
editor underlines them once they are written. To stop them being proposed at all, run
JSConsole: Toggle Inline Suggestions in Script Folders. It writes
"[javascript]": {"editor.inlineSuggest.enabled": false} into the settings of each workspace folder that
declares script folders (workspace settings in a single-folder window), so it covers all JavaScript in
that workspace folder, not just the declared subfolders. Running it again turns them back on. Declaring a
folder asks the same question once, with the reasons, when Copilot is installed; nothing is changed
without an answer.
What the editor cannot do is pick the argument for you. Nearly every parameter in the script API is
declared string, so every string in scope fits and knowing the type tells you nothing. Where the
repository accepts only a handful of values, this extension suggests those instead:
| Where the cursor is |
What is offered |
language: in a search |
the six query languages |
transaction: |
readonly, readwrite, none |
the first argument of selectNodes, luceneSearch, xpathSearch or tagSearch |
the repository's stores, live content first |
| a string holding a prefixed name |
types, aspects, properties and associations from the model |
The store list steps aside as soon as what you are typing stops being the start of one, so a path or a
query in that position gets name completion instead.
Names inside a query
A query is a string, so nothing in JavaScript objects to a misspelled namespace. The repository does not
object either: an unresolvable name matches nothing, the search returns an empty list, and the script
carries on as though the repository were empty. That silence is what these checks are for.
search.luceneSearch('PATH:"/acmeMial:attachments//*"');
// No namespace is registered with the prefix "acmeMial", so it cannot match anything.
search.query({ query: 'TYPE:"cm:contnet"', language: 'fts-alfresco' });
// "cm:contnet" is not a type in this repository's model. Did you mean cm:content?
Two things are reported, both of which can be stated without knowing your data. A prefix no namespace
registers, anywhere in the query, in any language. And TYPE: or ASPECT: naming a class the model
does not define. A prefixed name anywhere else is left alone: in a path the local part is a node's name
rather than a class, so flagging it would turn a check that is always right into one that cries wolf.
Completion works inside the string too. Typing a colon is the signal, so TYPE:"cm: offers types,
ASPECT:"cm: offers aspects, and only the half-written name is replaced rather than the whole query.
An ordinary string is left alone, since otherwise every one of them would open a list of a thousand names.
Both need the content model, so they stay quiet until definitions have been refreshed from the server.
Quick fixes
A misspelled QName, root object or query language is reported with "did you mean", and Ctrl+. applies the
correction. Suggestions rank an abbreviation ahead of an edit-distance match, so fts offers
fts-alfresco, and a transposition such as cm:naem counts as one mistake rather than two.
| Setting |
Default |
Description |
jsconsole.diagnostics.engineSyntax |
true |
Underline syntax the engine cannot parse. |
jsconsole.diagnostics.unknownGlobals |
true |
Report a root object the active profile does not have. |
jsconsole.diagnostics.searchQueries |
true |
Check query languages and Lucene field escaping. |
One scope per script
Each script the repository runs gets its own scope. Nothing declared in one is visible to another, and
a name borrowed from a sibling file fails at run time with "node" is not defined.
The bundled TypeScript plugin says so, with moduleDetection: force, and so does a generated
jsconfig.json where one is written. Without it TypeScript treats every .js file in the folder as one
shared script, so a var node at the top of one file is offered as a completion in all the others, and
the editor quietly suggests names that cannot work. Ambient declarations in the generated .d.ts stay
global either way, which is what the root objects need.
If a jsconfig.json of your own is there it is left alone, but a refresh says so when it has the shared
scope, because the only symptom is completions that look right and are not.
Where definitions are kept
Two profiles can point at repositories with completely different content models, so the raw payloads are
cached per profile in the extension's own storage, each stamped with the profile, its URL, the server
version and the fetch time. Each project gets a generated copy for the profile that is currently active:
the .d.ts files and the files listed below.
By default (jsconsole.typings.location set to storage) that copy lives outside the project, under
~/.jsconsole-for-alfresco/projects/, in a folder named after the project's absolute path. A TypeScript
server plugin bundled with the extension hands the declarations to VS Code's JavaScript service, pins
the standard library to what the server's engine has, and keeps each script in its own scope, so nothing
is written into the project. Set jsconsole.typings.location to workspace to keep the copy in the
project instead, in the folder named by jsconsole.typings.directory (.jsconsole). An in-project folder
that already exists is used whatever the setting says. Add it to your .gitignore: the files are
generated, describe one server, and can expose the shape of a custom model.
A jsconfig.json is the one file that cannot live outside the project, because the JavaScript service
finds it by walking up from an open file. With jsconsole.typings.jsConfig at auto it is written only
when the plugin could not be configured, and only when the folder has neither a jsconfig.json nor a
tsconfig.json. always writes it even with the plugin running, and never never does. A generated one
is removed the next time definitions are written with the plugin running; one you wrote is never touched.
That has three consequences worth knowing:
- Switching profiles regenerates the project's copy from the cache, with no download.
- If a cached set was taken from a different URL than the profile now points at, it is never used. The
extension refetches instead of quietly completing against the wrong server.
- Deleting or editing a profile discards its cache.
What the definitions folder contains
Besides the .d.ts files, the folder carries what the server said in a form anything can read, including
the language server and the MCP server:
| File |
What it is |
alfresco-*.d.ts |
The script API of this repository, for the TypeScript service. |
tern.json |
The API payload exactly as the server sent it, for the servers to answer API questions from. |
qnames.json |
Every type, aspect, property and association name, with its kind and owning class. |
classes.json |
The same model by class: what each type and aspect declares and inherits. |
globals.json |
The root objects this repository registers. |
engine.json |
What this server's JavaScript engine accepts, measured by the probe. |
meta.json |
Which profile, URL and server version the folder describes, when it was read, and which build wrote it. |
recipes.json |
Your own recipes, when jsconsole.recipes.folders names any, for the MCP server. |
README.md |
Which profile and server the folder describes, and how to write scripts for it. |
qnames.json and classes.json need the content model, and engine.json the probe; each is left out
when that part of the refresh could not be read.
That README is deliberately specific. Telling a tool "Alfresco probably rejects let" ages badly and is
wrong as often as not, while naming the measured result for the server in play does not.
The language server reads the definitions itself at startup, so it needs nothing from this extension. It
looks in the project first and then in storage, working the storage folder out from the project's path,
so either location works. Point any LSP client at node dist/server.cjs --stdio in a project that has
been refreshed and you get the same completions, hovers, quick fixes and diagnostics. The MCP server
finds them the same way.
Run history
A rerun goes back to the profile the run was recorded on, not whichever is active. Rerunning a script
recorded against a development server while a production profile is selected would otherwise run it
against production, and the only sign would be the profile name in the output header, after the fact.
When the recorded profile is not the active one, the rerun is confirmed first and names both. When it
has been deleted, the rerun is refused rather than quietly sent somewhere else. The guardrails and the
run-as check both use the recorded profile too, so a production tier is treated as production.
The parameters come from the record as well, not from the status bar: the transaction mode, the run-as
user, the space and document nodes, the URL arguments and the dump limit. The run header names the ones
in force, so a rerun that used something other than the current selection says so on its first line.
Every run is written to the Run History view, grouped by profile and newest
first. Each entry keeps the script exactly as it was sent, the parameters it ran with, the output, and how
it ended. Click an entry to read its output; right-click for the rest:
- Rerun sends the stored script again, with its stored parameters, through the same guardrails. It
goes to the profile the run was recorded on, confirming first when that is not the active one, as
described above.
- Open script puts the stored source in a new editor, with a comment naming where it came from.
- Diff output with… picks a second entry and opens both outputs in a diff editor. This is the quickest
way to see what changed between two runs of the same script, or the same script on two servers.
- Delete removes one entry; Clear empties a profile's history after confirming.
| Setting |
Default |
Description |
jsconsole.history.maxEntries |
200 |
Runs kept per profile. Older ones are deleted. 0 turns the history off. |
jsconsole.history.maxOutputKb |
512 |
Size ceiling per run. Dumps, rendered templates, then the middle of the output are dropped to fit. |
History lives in the extension's own storage folder, not in your workspace, so it never reaches a
repository. It is stored as plain JSON and is not encrypted, so treat anything a script prints the same way
you treat the script itself. The script is never truncated, because rerunning it is the point.
Environment guardrails
Every profile has an environment: development, test or production. You pick it when adding the
profile, and it changes how hard it is to run something destructive. Profiles created before this existed
count as development.
| Run |
Development |
Test |
Production |
| Read-only |
runs |
runs |
runs |
| Read-write or no transaction |
confirmation dialog |
confirmation dialog |
type the profile name |
| Run as another user |
runs |
runs |
type the profile name, once per session |
Both read-write and "no transaction" count as writes: without a surrounding transaction the script is free to
open its own and commit. jsconsole.execution.confirmReadWrite turns the dialog off for development profiles
only; test and production always ask.
A production profile can additionally be set to never allow writes. It then refuses read-write and
no-transaction runs outright, whatever the settings, the status bar or an @jsconsole header line ask for.
Use it for the servers nobody should be writing to from an editor.
The active profile sits in the status bar: red for production, orange for test. The run header in the
JSConsole Output output names the environment too, for example
▶ prod-eu · [PROD] · readonly.
Authentication modes
Basic. User name and password. The password is stored in VS Code secret storage.
External (trusted header). For repositories whose authentication.chain contains external. The extension
sends the remote user name in the header configured as external.authentication.proxyHeader
(X-Alfresco-Remote-User by default), exactly as a reverse proxy would after SSO. Use it when:
- the repository is reachable directly, typically development and test environments, or
external.authentication.proxyUserName is set and the profile presents the proxy's TLS client certificate
(PKCS#12 or PEM certificate plus key; the passphrase goes to secret storage).
Without one of those, the repository would trust any caller that sets the header, which is a server
misconfiguration rather than something this extension can fix.
HTTPS and certificates
A repository behind a company certificate authority, or on a development box with a self-signed
certificate, will not verify against the roots the machine already trusts. When that happens the error
names the cause and offers the fix, and JSConsole: Certificate Trust for the Active Profile... reaches
the same choices at any time.
| Choice |
What it does |
When |
| Import from server |
Reads the chain the server presents, shows it with its SHA-256 fingerprint, and saves the authority above the server certificate as the profile's CA |
A company or internal CA. The preferred answer |
| Choose CA file... |
Validates against a PEM you already have |
Your administrators published the CA |
| Trust this server |
Skips verification: accepts any certificate for this profile |
Throwaway development servers only |
The error offers the choices that fit the failure: all three for an unknown authority, Edit profile
and Trust this server for a host name mismatch, and only Trust this server for a certificate that
has expired or is not valid yet. The certificate trust command lists the same three, with the last
labelled Skip verification, plus Validate normally to forget a CA or stop skipping.
The first two keep validation on: the host name, the expiry and the signature are all still checked, and
the CA is added to the ones already trusted rather than replacing them. An imported CA is written to the
extension's global storage as certificates/<profile id>.pem and removed when the profile is deleted.
Importing reads the chain over the very connection that could not be verified, so it proves nothing by
itself. That is why the subject names and the fingerprint are shown before anything is saved: check them
against what your administrators published.
Skipping verification is different in kind, not degree. It accepts any certificate for that profile, so
anything able to answer on the host is trusted and the traffic can be read or altered in transit without
showing. It is confirmed with a modal that says so, and a profile on a test or prod tier is named as
such in the confirmation.
Getting started
JSConsole: Add Server Profile. Enter a name, the repository URL (for example http://localhost:8080/alfresco),
the user name and the password.
JSConsole: Test Connection to confirm the console is reachable.
- Declare the folder that holds your scripts: right-click it in the Explorer and choose JSConsole for
Alfresco › Declare Folder as Alfresco Scripts.
JSConsole: Refresh API Typings from Server. This reads the server's script API, content model and
engine, caches them with the profile, and gives the editor completion and checks for them. By default
nothing is written into the project; see Where definitions are kept.
- Open a
.js file, write a script, press Ctrl+Alt+R.
var results = search.query({ query: 'TYPE:"cm:content"', language: 'fts-alfresco' });
print(results.length + " documents");
for (var i = 0; i < results.length && i < 5; i++) {
print(results[i].properties["cm:name"]);
}
Settings
| Setting |
Default |
Description |
jsconsole.execution.transaction |
readonly |
readonly, readwrite or none. |
jsconsole.execution.runAs |
|
Run scripts as this user. |
jsconsole.execution.confirmReadWrite |
true |
Ask before running in a read-write transaction. |
jsconsole.execution.pollIntervalMs |
500 |
How often print output is fetched while a script runs. |
jsconsole.typings.location |
storage |
storage keeps a project's definitions under ~/.jsconsole-for-alfresco/projects/; workspace writes them into the project. An existing in-project folder is used either way. |
jsconsole.typings.directory |
.jsconsole |
The in-project folder name used when jsconsole.typings.location is workspace. |
jsconsole.typings.jsConfig |
auto |
Write a jsconfig.json into the project: auto only when the TypeScript plugin could not be configured, always, or never. |
jsconsole.typings.includeWebScriptApi |
true |
Also generate typings for web script root objects (model, args, url, ...). |
jsconsole.typings.refreshOnProfileSwitch |
true |
Read a profile's server the first time you switch to it. A profile read before switches instantly from its cache. |
jsconsole.typings.maxAgeDays |
7 |
Definitions older than this count as stale. 0 disables the check. |
jsconsole.typings.staleAction |
prompt |
What happens when they are stale, at startup or on a switch: prompt asks, offering Refresh, Always Refresh, Not Now and Never Ask; refresh reads them again without asking; ignore leaves them. Always and Never set this in your user settings. |
jsconsole.recipes.enabled |
true |
Offer recipes in completion, in files recognised as Alfresco scripts. |
jsconsole.recipes.folders |
[] |
Folders of your own recipes. Read from user, workspace and folder settings together. |
jsconsole.scriptApi.includeWebScriptGlobals |
false |
List the web script root objects in the Script API view. |
The settings for the Scripts view, diagnostics and run history are in their own sections above.
Typings are a snapshot of the server. Refresh them after deploying a content model or an addon that adds
script root objects; the stale check reminds you when the active profile has not been read for a while.
How it works
| Action |
Endpoint on the repository |
| Run script |
POST /alfresco/s/ootbee/jsconsole/execute |
| Stream output / recover result |
GET /alfresco/s/ootbee/jsconsole/{channel}/executionResult |
| Test connection |
GET /alfresco/s/ootbee/jsconsole/serverInfo |
| API definitions |
GET /alfresco/s/ootbee/jsconsole/tern-definitions/alfresco-script-api |
| Content model |
GET /alfresco/s/api/classes (stock Alfresco, not OOTBee) |
| Engine probe, run-as check |
POST /alfresco/s/ootbee/jsconsole/execute, read-only |
The execute request stays open for the whole run. If a proxy cuts it, the extension keeps polling the result
endpoint, where the server caches the outcome under the channel id.
OOTBee publishes print() output in chunks of five lines, so short scripts show all output at the end while
long-running scripts stream as they go.
The extension activates when a JavaScript file is opened (onLanguage:javascript), when a workspace
contains an in-project .jsconsole/ folder with generated typings, when a notebook is opened, or when
one of its commands or views is used. Because any JavaScript project activates it, definitions are written
automatically only for a folder that has been declared or already has them from a refresh.
Generated typings are a translation of the Tern definitions OOTBee produces by reflecting over the running
repository. Everything is placed in a declare namespace Alfresco; root objects are declared as globals.
Troubleshooting
- Nothing happens after Run. Open Output and pick the JSConsole Log channel. Every request and
response is logged there with status and timing (
Developer: Set Log Level to Debug also shows the
result polling). The JSConsole Output channel holds the script output itself, and JSConsole
Language Server the language server's own log.
- "A script is already running". The previous run has not finished. The message offers to cancel it;
the progress notification for a run also has a Cancel button.
- Password prompt appears and the run stops. The stored password is missing or was rejected. Use
JSConsole: Set Profile Password and run again. Background polling never prompts.
- 404 on every call. The profile URL must point at the repository web application, for example
http://localhost:8080/alfresco, and OOTBee Support Tools must be installed there.
- "Cannot verify ". The server certificate did not validate. The message says why, and offers to
import the authority, point at a CA file, or trust the server without verifying it. See
HTTPS and certificates.
- A profile you no longer want.
JSConsole: Delete Server Profile removes it together with its stored
password, client certificate passphrase, imported CA and cached definitions.
Known upstream issues
Behaviour of the OOTBee JavaScript Console that surprised us, verified against Alfresco Content Services
Community 25.2.0 with OOTBee Support Tools 1.2.3.0. None of these is caused by this extension, and none is fixable from the client.
They are recorded here so they can be rechecked against a newer OOTBee release, and ideally fixed upstream.
1. dump() inside a script produces no output.
DumpService.addDump builds and returns a fresh list on every call. The console's pre-roll dump()
wrapper throws that return value away, and ExecuteWebscript only stores the list from its own final
addDump(documentNode). So the dump section contains the node set as the document parameter and
nothing the script dumped. Verified by dumping one node while pointing the document parameter at another:
the response described the document. Our workaround: samples/03-dump.js shows the document-parameter
path and prints properties instead. Likely fix upstream: have the pre-roll accumulate what it dumps and
have the webscript store that.
2. An unknown run-as user is accepted silently.
AuthenticationUtil.runAs pushes whatever string it is given; nothing checks that the person exists. The
run then proceeds with only GROUP_EVERYONE. On a default repository that still reads most content, so
nothing appears to be wrong. Our workaround: the extension checks the name and warns. Arguably not a
bug in Alfresco itself, since runAs is a low-level primitive, but the console exposes it to people who
reasonably expect a typo to be caught.
3. person and userhome ignore run-as.
The script model is built from the authenticated user, so under runas=alice the script still sees
person.properties["cm:userName"] === "admin" while permission checks correctly use alice. Half the
model honours run-as and half does not. Our workaround: documentation only, plus
samples/06-run-as-and-people.js, which prints both identities side by side. Likely fix upstream:
resolve person and userhome from AuthenticationUtil.getRunAsUser() when a run-as user is set.
4. transaction=none drops half the script model, and fails outright without a Space.
Company Home, the user's home and the person node are resolved through calls that need a transaction, and
in this mode the console does not open one. So companyhome, userhome, person and document are
never put into the script scope at all: using one raises
ReferenceError: "companyhome" is not defined, which reads as though the repository were missing it.
Worse, space defaults to Company Home, so with no Space parameter it is null too, and the webscript then
fails while building its response:
Cannot invoke "org.alfresco.repo.jscript.ScriptNode.getNodeRef()" because "newSpace" is null. That
happens after the script has run and regardless of what the script contains, so in this mode every
run fails, including var x = 1 + 1;.
Set a Space parameter and a run succeeds, with space present and the four node globals still absent.
Everything else is there — search, people, siteService, actions, utils, logger, session,
args, model, workflow, paging, classification, preferenceService, activities — and ordinary
repository reads still work, because Alfresco opens its own transaction per service call. All measured by
listing typeof for each global in both modes. Our workaround: the extension warns before a run in this
mode. Likely fix upstream: resolve the node model inside a short read-only transaction whatever the
requested mode, or reject none with an explanation.
5. A paged folder listing never says it is finished. childFileFolders(files, folders, ignoreTypes, skip, max, …) returns a page whose hasMoreItems() is a Java Boolean object rather than a JavaScript
boolean, and an object is truthy even when it holds false. So if (!result.hasMoreItems()) break; never
breaks: the script keeps requesting empty pages on the server until the repository is restarted, since
aborting the request does not stop it. String(result.hasMoreItems()) and result.hasMoreItems() == false
read it correctly. This paged form is also missing from the API payload, which lists only the zero- and
two-argument forms. Measured on ACS 25.2, the hard way. Our workaround: the children recipe reads it
through String and also stops on an empty page. Likely fix upstream: return a primitive boolean.
6. A caught repository error throws away the whole read-write run, and the run reports success.
Catch an error the repository threw — a name that already exists, a protected node, a bad workflow
transition, even a failed lookup such as workflow.getTask("147") — and carry on, and everything the run
did is rolled back when it ends. The console answers exactly as it does for a run that committed: HTTP
200, the same fields, the script's own output, nothing to say it was discarded. An error from the
script's own code (a TypeError, a throw) can be caught without this. Measured on ACS 25.2 with six
kinds of error, each after a create in the same run. Our workaround: the errors recipe rethrows any
JavaException, so the run fails where it can be seen, and the create recipe looks before it writes.
Likely fix upstream: report the rollback in the response, or fail the run when the transaction it
commits is already marked rollback-only.
Development
npm install
npm run build # bundles dist/extension.cjs and dist/server.cjs
npm run watch
npm test # unit and mock-server tests (node --test)
npm run typecheck
npm run docs:recipes # regenerates docs/RECIPES.md from the catalogue; a test fails when it is stale
npm run package # builds the .vsix
The repository carries no launch configuration; CONTRIBUTING.md has one to start from, so
F5 opens an Extension Development Host. Node 22.18 or newer is required to run the tests, which execute
the TypeScript sources directly.
CONTRIBUTING.md has the rest: how the tests are put together, how to debug either
server, the conventions the code follows, and why measuring against a live repository matters more here
than it usually does.
Roadmap
- Ticket and OIDC (Keycloak / Identity Service) authentication via a VS Code
AuthenticationProvider.
- Saved scripts (the console's list/save endpoints).