@webship.co/workspace 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/docs/releases.md +11 -2
- package/core/workspace-app/PLUGINS.md +1 -1
- package/core/workspace-app/package.json +1 -1
- package/core/workspace-app/plugin-loader.js +1 -1
- package/core/workspace-app/server.js +89 -52
- package/core/workspace-app/tests/unit/images-svg.test.js +7 -1
- package/core/workspace-app/tests/unit/node-menu.test.js +5 -1
- package/core/workspace-app/tests/unit/row-menu-fragments.test.js +39 -5
- package/package.json +1 -1
package/core/docs/releases.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
-
Every package is versioned together: `@webship.co/workspace` (the core) and every `@webship.co/workspace-<name>`. Versions so far: 1.0.0-alpha1 (the first publish), 1.0.0-beta1.
|
|
3
|
+
Every package is versioned together: `@webship.co/workspace` (the core) and every `@webship.co/workspace-<name>`. Versions so far: 1.0.0-alpha1 (the first publish), 1.0.0-beta1, 1.0.0-beta2, 1.0.0-rc1, 1.0.0, 1.0.1 (the core and the git, ide and graph plugins only), 1.0.2.
|
|
4
4
|
|
|
5
5
|
## How a change lands
|
|
6
6
|
|
|
@@ -10,7 +10,16 @@ Every package is versioned together: `@webship.co/workspace` (the core) and ever
|
|
|
10
10
|
|
|
11
11
|
## Publishing
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
One package at a time, within npm's publishing limits, in this order: the types (`workspace-plugin-api`), the bases (`workspace-hosting`, `workspace-forge`, `workspace-container`), the providers and the other plugins, and the core last. A dependency on a sibling is a version range (`^1.0.2`), never a git URL.
|
|
14
|
+
|
|
15
|
+
For each package:
|
|
16
|
+
|
|
17
|
+
1. Set `version` in `package.json` and every `@webship.co/workspace-*` range to the version of the round.
|
|
18
|
+
2. Run `npm install` so that `package-lock.json` resolves the siblings already published in this round, and commit the two files together. CI runs `npm ci`, which refuses a lock file that is out of step with `package.json`.
|
|
19
|
+
3. Push `1.0.x` with the `v<version>` tag, publish with `npm publish --access public`, and create the GitHub release.
|
|
20
|
+
4. Before moving on to a package that depends on this one, wait until `npm view @webship.co/workspace-<name>@<version> version` answers: the registry takes a minute or two to serve a new version, and a dependent pushed before that fails its CI with `No matching version found`.
|
|
21
|
+
|
|
22
|
+
The 1.0.0 round pushed every version commit before any package was published and left the regenerated lock files uncommitted, so the CI of every plugin was red from that commit until 1.0.2.
|
|
14
23
|
|
|
15
24
|
## Upgrading a workspace
|
|
16
25
|
|
|
@@ -205,7 +205,7 @@ it alike), so a lazy menu behaves like an inline one once open. A string return
|
|
|
205
205
|
menu, inline. A plugin that must also run on a dashboard older than this form checks
|
|
206
206
|
`api.capabilities && api.capabilities.has('row-menu-body')` and falls back to the string.
|
|
207
207
|
|
|
208
|
-
The core's own menus take the same road: DDEV, COMPOSER, Tests and
|
|
208
|
+
The core's own menus take the same road: DDEV, COMPOSER, Tests, SCREENS, NODE and More are drawn as this shell and
|
|
209
209
|
answer at `/fragments/row-menu/<slot>/<ws>/<project>` with no `body` parameter, their items computed for
|
|
210
210
|
that one project when the menu opens (a page of fifty rows is a third of what it was). A plugin's
|
|
211
211
|
`body` must still name one of its own fragments; the core slots are not open to one.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workspace-dashboard",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "The Webship Workspace dashboard: a lean core (workspaces, builders, settings, the assistant shell, the plugin system) that every feature joins as a @webship.co/workspace-<name> plugin — required with npm, then activated, installed and configured.",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"main": "server.js",
|
|
@@ -414,7 +414,7 @@ function rowMenuHtml(slot, result, ctx = {}) {
|
|
|
414
414
|
|
|
415
415
|
/**
|
|
416
416
|
* The shell of a row menu whose body loads when it opens — the one markup for a plugin's lazy menu and for
|
|
417
|
-
* the core's own (DDEV, COMPOSER, Tests, SCREENS): `<span class="act-with-status">` around the
|
|
417
|
+
* the core's own (DDEV, COMPOSER, Tests, SCREENS, NODE, More): `<span class="act-with-status">` around the
|
|
418
418
|
* `.uk-inline.act-menu.act-menu-<slot>` wrapper, the button (`html`), an empty `uk-dropdown` shell and the
|
|
419
419
|
* caption. The shell fetches `/fragments/row-menu/<slot>/<ws>/<project><query>` on `menu-opened` (the relay
|
|
420
420
|
* of UIkit's `beforeshow` in public/ui.js) and swaps the answer in as its inner HTML; `triggers` adds more
|
|
@@ -1018,13 +1018,36 @@ function composerMenuBody(key, p, projectPath, isDdev, mode = 'manifest') {
|
|
|
1018
1018
|
* left, a package and the package.json scripts on the right. What runs lives in actions/node.js.
|
|
1019
1019
|
*/
|
|
1020
1020
|
function nodeMenuHtml(key, p, projectPath, isDdev) {
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
|
|
1021
|
+
const state = nodeMenuState(projectPath);
|
|
1022
|
+
if (!state) return '';
|
|
1023
|
+
// The row carries the button and the caption; the body (nodeMenuBody) arrives from
|
|
1024
|
+
// /fragments/row-menu/node/<ws>/<project> when the menu opens — fifty rows, fifty buttons, no menu (#111).
|
|
1025
|
+
return menuShellHtml('node', {
|
|
1026
|
+
key, project: p, caption: state.caption, dropdownClasses: 'node-dropdown',
|
|
1027
|
+
html: '<button class="uk-button uk-button-default uk-button-small" type="button" title="npm and yarn for this folder"><span uk-icon="icon: nut; ratio: .7"></span> NODE <span uk-icon="icon: chevron-down; ratio: .6"></span></button>',
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/** What the NODE caption says: package.json parsed (or not), and which lock files sit beside it. Null without a package.json. */
|
|
1032
|
+
function nodeMenuState(projectPath) {
|
|
1033
|
+
const { hasPackageJson, nodeInfo } = require('./actions/node');
|
|
1034
|
+
if (!hasPackageJson(projectPath)) return null;
|
|
1024
1035
|
const info = nodeInfo(projectPath);
|
|
1025
|
-
const lockFile = { yarn: 'yarn.lock', npm: 'package-lock.json' };
|
|
1026
1036
|
const caption = !info.valid ? 'bad json'
|
|
1027
1037
|
: (info.locks.yarn && info.locks.npm ? '2 locks' : (info.locks.yarn ? 'yarn.lock' : (info.locks.npm ? 'npm lock' : 'no lock')));
|
|
1038
|
+
return { info, caption };
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* The NODE menu's body — what used to sit inside the dropdown on every row — for one project, served by
|
|
1043
|
+
* /fragments/row-menu/node/<ws>/<project> when the menu opens. '' for a folder without package.json.
|
|
1044
|
+
*/
|
|
1045
|
+
function nodeMenuBody(key, p, projectPath, isDdev) {
|
|
1046
|
+
const { MANAGERS, NODE_GROUPS, NODE_RUN_ACTIONS, NODE_PACKAGE_ACTIONS, NODE_SCRIPT_ROUTE, nodeArgsFor, nodeScripts } = require('./actions/node');
|
|
1047
|
+
const state = nodeMenuState(projectPath);
|
|
1048
|
+
if (!state) return '';
|
|
1049
|
+
const { info } = state;
|
|
1050
|
+
const lockFile = { yarn: 'yarn.lock', npm: 'package-lock.json' };
|
|
1028
1051
|
const vals = (m, extra = '') => `hx-vals='{"workspace":"${esc(key)}","projectName":"${esc(p)}","manager":"${m}"${extra}}' hx-target="#webship-workspace-output" hx-swap="innerHTML"`;
|
|
1029
1052
|
const scripts = info.valid ? nodeScripts(info.pkg) : [];
|
|
1030
1053
|
const panel = (m) => {
|
|
@@ -1063,17 +1086,12 @@ function nodeMenuHtml(key, p, projectPath, isDdev) {
|
|
|
1063
1086
|
</div></li>`;
|
|
1064
1087
|
};
|
|
1065
1088
|
const tabs = MANAGERS.map((m) => `<li><a href="#">${m} <span class="uk-text-meta">· ${info.locks[m] ? 'lock ✓' : 'no lock'}</span></a></li>`).join('');
|
|
1066
|
-
return `<
|
|
1067
|
-
<button class="uk-button uk-button-default uk-button-small" type="button" title="npm and yarn for this folder"><span uk-icon="icon: nut; ratio: .7"></span> NODE <span uk-icon="icon: chevron-down; ratio: .6"></span></button>
|
|
1068
|
-
<div uk-dropdown="mode: click; pos: bottom-right" class="node-dropdown">
|
|
1069
|
-
<ul class="uk-nav uk-dropdown-nav">
|
|
1089
|
+
return `<ul class="uk-nav uk-dropdown-nav">
|
|
1070
1090
|
<li class="uk-nav-header">Node · ${esc((info.pkg && typeof info.pkg.name === 'string' && info.pkg.name) || p)}</li>
|
|
1071
1091
|
<li data-entry="how it runs"><span class="uk-text-meta">${isDdev ? 'Runs as ddev npm / ddev yarn in this project.' : "Not a DDEV project — runs in the dashboard's DDEV container."}${info.pinned ? ` packageManager: ${esc(info.pinned.spec)}.` : ''}${info.nodeModules ? ' node_modules is here.' : ' No node_modules yet.'}${info.valid ? '' : ' package.json is not valid JSON.'}</span></li>
|
|
1072
1092
|
</ul>
|
|
1073
1093
|
<ul class="uk-tab node-tabs" data-keep-open uk-tab="active: ${Math.max(0, MANAGERS.indexOf(info.preferred))}">${tabs}</ul>
|
|
1074
|
-
<ul class="uk-switcher">${MANAGERS.map(panel).join('')}</ul
|
|
1075
|
-
</div>
|
|
1076
|
-
</div><span class="act-status uk-text-meta">${esc(caption)}</span></span>`;
|
|
1094
|
+
<ul class="uk-switcher">${MANAGERS.map(panel).join('')}</ul>`;
|
|
1077
1095
|
}
|
|
1078
1096
|
|
|
1079
1097
|
|
|
@@ -3285,6 +3303,46 @@ function ddevMenuItemsHtml({ key, p, meta, projectPath, gates, ddev }) {
|
|
|
3285
3303
|
* workspace's type must WANT the action (policy — `type:` in its settings file). Read once per page by
|
|
3286
3304
|
* the rows, and once per request by a menu body served on its own.
|
|
3287
3305
|
*/
|
|
3306
|
+
/**
|
|
3307
|
+
* The More menu's items for one project — the changelog, the recordings, the backups and the remove —
|
|
3308
|
+
* before the Buttons settings arrange them. The row builds them to count what the caption says
|
|
3309
|
+
* ("3 actions") and /fragments/row-menu/more/<ws>/<project> builds them again when the menu opens (#111).
|
|
3310
|
+
* Both verbs of the teardown also live in the DDEV menu, where a DDEV project's teardown belongs. They
|
|
3311
|
+
* stay here too: More is where someone looks for them, and a folder DDEV does not manage has no other home.
|
|
3312
|
+
*/
|
|
3313
|
+
function moreMenuItemsHtml({ key, p, gates }) {
|
|
3314
|
+
const { canBackup, canRemove } = gates;
|
|
3315
|
+
const items = [];
|
|
3316
|
+
// What this product shipped, read from the CHANGELOG it already keeps. It belongs to the
|
|
3317
|
+
// row rather than to the rail: a changelog is a fact about one product, and the question
|
|
3318
|
+
// people ask is "what went into 1.0.3 of THIS", standing in front of it in its folder.
|
|
3319
|
+
// The latest version is in the label, so the menu answers the common case unopened.
|
|
3320
|
+
const cl = changelog.changelogFor(key, p);
|
|
3321
|
+
if (cl) {
|
|
3322
|
+
const latest = (changelog.changelogIndex().find((r) => r.workspace === key && r.item === p) || {}).latest || '';
|
|
3323
|
+
items.push(`<li><a href hx-get="/fragments/changelog/${encodeURIComponent(key)}/${encodeURIComponent(p)}"
|
|
3324
|
+
hx-target="#editor-modal-body" hx-swap="innerHTML" uk-toggle="target: #editor-modal"
|
|
3325
|
+
title="Releases, newest first, from ${esc(cl.file)}"><span uk-icon="icon: history; ratio: .7"></span> Changelog${latest ? ` <span class="uk-text-meta">${esc(latest)}</span>` : ''}</a></li>`);
|
|
3326
|
+
}
|
|
3327
|
+
// Recordings about this project: a video, or audio, started from a set of pre-prompts.
|
|
3328
|
+
if (projectMake.WORKSPACES.includes(key)) {
|
|
3329
|
+
const makeLink = (kind, icon, label, hint) => `<li data-entry="${esc(label.toLowerCase())}"><a href hx-get="/fragments/projects/make-form/${encodeURIComponent(key)}/${encodeURIComponent(p)}?kind=${kind}"
|
|
3330
|
+
hx-target="#editor-modal-body" hx-swap="innerHTML" hx-on::after:request="UIkit.modal('#editor-modal').show()"
|
|
3331
|
+
title="${esc(hint)}"><span uk-icon="icon: ${icon}; ratio: .7"></span> ${esc(label)}…</a></li>`;
|
|
3332
|
+
items.push(makeLink('video', 'play-circle', 'Create a video', 'A narrated video about this project, from a set of pre-prompts'));
|
|
3333
|
+
items.push(makeLink('audio', 'microphone', 'Create audio', 'A spoken recording about this project, from a set of pre-prompts'));
|
|
3334
|
+
items.push('<li class="uk-nav-divider"></li>');
|
|
3335
|
+
}
|
|
3336
|
+
if (canBackup) items.push(`<li><a href="${wsUrl(key, '/backups')}"><span uk-icon="icon: album; ratio: .7"></span> Backups for this workspace</a></li>`);
|
|
3337
|
+
if (canRemove) {
|
|
3338
|
+
items.push('<li class="uk-nav-divider"></li>');
|
|
3339
|
+
items.push(`<li><a href class="uk-text-danger" hx-get="/fragments/projects/remove-form/${encodeURIComponent(key)}/${encodeURIComponent(p)}" hx-target="#editor-modal-body" hx-swap="innerHTML"
|
|
3340
|
+
hx-on::after:request="UIkit.modal('#editor-modal').show()"
|
|
3341
|
+
title="Asks about a backup, then confirms, then removes"><span uk-icon="icon: trash; ratio: .7"></span> Remove the project…</a></li>`);
|
|
3342
|
+
}
|
|
3343
|
+
return items.join('');
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3288
3346
|
function rowGates(dir, meta) {
|
|
3289
3347
|
const allowed = meta.typeActions;
|
|
3290
3348
|
return {
|
|
@@ -3462,7 +3520,8 @@ async function screensMenuBody(ws, project) {
|
|
|
3462
3520
|
|
|
3463
3521
|
// The row menus the core draws itself whose bodies load when they open — the slots
|
|
3464
3522
|
// /fragments/row-menu/<slot>/<ws>/<project> answers with no `body` parameter (coreRowMenuBody).
|
|
3465
|
-
|
|
3523
|
+
// NODE and More joined in #111: a row carries their buttons and captions, not their dropdowns.
|
|
3524
|
+
const CORE_ROW_MENUS = new Set(['ddev', 'composer', 'tests', 'screens', 'node', 'more']);
|
|
3466
3525
|
|
|
3467
3526
|
/**
|
|
3468
3527
|
* The body of one of the core's row menus for one project, as `{ status, html }`: the dropdown's inner
|
|
@@ -3498,19 +3557,27 @@ async function coreRowMenuBody(slot, key, project) {
|
|
|
3498
3557
|
const { nav, oob } = await screensMenuBody(key, project);
|
|
3499
3558
|
return { status: 200, html: `<ul class="uk-nav uk-dropdown-nav">${nav}</ul>${oob}` };
|
|
3500
3559
|
}
|
|
3560
|
+
if (slot === 'node') {
|
|
3561
|
+
const body = nodeMenuBody(key, project, projectPath, isDdev);
|
|
3562
|
+
return body ? { status: 200, html: body } : none('No package.json here.');
|
|
3563
|
+
}
|
|
3564
|
+
if (slot === 'more') {
|
|
3565
|
+
const items = moreMenuItemsHtml({ key, p: project, gates });
|
|
3566
|
+
return items ? { status: 200, html: `<ul class="uk-nav uk-dropdown-nav">${items}</ul>` } : none('Nothing more to offer here.');
|
|
3567
|
+
}
|
|
3501
3568
|
return none('No such menu.');
|
|
3502
3569
|
}
|
|
3503
3570
|
|
|
3504
3571
|
/**
|
|
3505
3572
|
* The Buttons settings page lists what each menu has been seen to hold (row-layout's seen record). A
|
|
3506
3573
|
* menu whose body loads when it opens (coreRowMenuBody) is recorded when one opens, so on a dashboard
|
|
3507
|
-
* where nobody has opened COMPOSER or
|
|
3574
|
+
* where nobody has opened COMPOSER, Tests or NODE yet the page would list nothing for them: before it is drawn,
|
|
3508
3575
|
* each such menu with nothing on record is built once, for the first project that carries it, and
|
|
3509
3576
|
* arranged — which is what records it. DDEV is counted on every row, so it is on record already, and
|
|
3510
3577
|
* SCREENS lists whatever sessions happen to run, which is nothing to learn from.
|
|
3511
3578
|
*/
|
|
3512
3579
|
async function learnCoreMenus() {
|
|
3513
|
-
for (const slot of ['composer', 'tests']) {
|
|
3580
|
+
for (const slot of ['composer', 'tests', 'node']) {
|
|
3514
3581
|
if (Object.keys(rowLayout.seenMenu(slot).groups).length) continue;
|
|
3515
3582
|
let learned = false;
|
|
3516
3583
|
for (const [key, meta] of Object.entries(loadWorkspaces())) {
|
|
@@ -3727,47 +3794,17 @@ async function projectRowsHtml(key, dir, state) {
|
|
|
3727
3794
|
</div><span class="act-status uk-text-meta">${!rag.up ? 'offline' : built ? 'indexed' : 'none'}</span></span>`;
|
|
3728
3795
|
})() : '';
|
|
3729
3796
|
const moreMenu = (() => {
|
|
3730
|
-
// Both verbs also live in the DDEV menu, where a DDEV project's
|
|
3731
|
-
// teardown belongs. They stay here too: More is where someone looks
|
|
3732
|
-
// for them, and a folder DDEV does not manage has no other home.
|
|
3733
|
-
const items = [];
|
|
3734
|
-
// What this product shipped, read from the CHANGELOG it already keeps. It belongs to the
|
|
3735
|
-
// row rather than to the rail: a changelog is a fact about one product, and the question
|
|
3736
|
-
// people ask is "what went into 1.0.3 of THIS", standing in front of it in its folder.
|
|
3737
|
-
// The latest version is in the label, so the menu answers the common case unopened.
|
|
3738
|
-
const cl = changelog.changelogFor(key, p);
|
|
3739
|
-
if (cl) {
|
|
3740
|
-
const latest = (changelog.changelogIndex().find((r) => r.workspace === key && r.item === p) || {}).latest || '';
|
|
3741
|
-
items.push(`<li><a href hx-get="/fragments/changelog/${encodeURIComponent(key)}/${encodeURIComponent(p)}"
|
|
3742
|
-
hx-target="#editor-modal-body" hx-swap="innerHTML" uk-toggle="target: #editor-modal"
|
|
3743
|
-
title="Releases, newest first, from ${esc(cl.file)}"><span uk-icon="icon: history; ratio: .7"></span> Changelog${latest ? ` <span class="uk-text-meta">${esc(latest)}</span>` : ''}</a></li>`);
|
|
3744
|
-
}
|
|
3745
|
-
// Recordings about this project: a video, or audio, started from a set of pre-prompts.
|
|
3746
|
-
if (projectMake.WORKSPACES.includes(key)) {
|
|
3747
|
-
const makeLink = (kind, icon, label, hint) => `<li data-entry="${esc(label.toLowerCase())}"><a href hx-get="/fragments/projects/make-form/${encodeURIComponent(key)}/${encodeURIComponent(p)}?kind=${kind}"
|
|
3748
|
-
hx-target="#editor-modal-body" hx-swap="innerHTML" hx-on::after:request="UIkit.modal('#editor-modal').show()"
|
|
3749
|
-
title="${esc(hint)}"><span uk-icon="icon: ${icon}; ratio: .7"></span> ${esc(label)}…</a></li>`;
|
|
3750
|
-
items.push(makeLink('video', 'play-circle', 'Create a video', 'A narrated video about this project, from a set of pre-prompts'));
|
|
3751
|
-
items.push(makeLink('audio', 'microphone', 'Create audio', 'A spoken recording about this project, from a set of pre-prompts'));
|
|
3752
|
-
items.push('<li class="uk-nav-divider"></li>');
|
|
3753
|
-
}
|
|
3754
|
-
if (canBackup) items.push(`<li><a href="${wsUrl(key, '/backups')}"><span uk-icon="icon: album; ratio: .7"></span> Backups for this workspace</a></li>`);
|
|
3755
|
-
if (canRemove) {
|
|
3756
|
-
items.push('<li class="uk-nav-divider"></li>');
|
|
3757
|
-
items.push(`<li><a href class="uk-text-danger" hx-get="/fragments/projects/remove-form/${encodeURIComponent(key)}/${encodeURIComponent(p)}" hx-target="#editor-modal-body" hx-swap="innerHTML"
|
|
3758
|
-
hx-on::after:request="UIkit.modal('#editor-modal').show()"
|
|
3759
|
-
title="Asks about a backup, then confirms, then removes"><span uk-icon="icon: trash; ratio: .7"></span> Remove the project…</a></li>`);
|
|
3760
|
-
}
|
|
3761
3797
|
// Arranged before it is counted, like DDEV — and whether there is a More at all is decided after,
|
|
3762
|
-
// since hiding can empty it and a custom action can fill it.
|
|
3763
|
-
|
|
3798
|
+
// since hiding can empty it and a custom action can fill it. The row carries the button and the
|
|
3799
|
+
// count; the items arrive from /fragments/row-menu/more/<ws>/<project> when the menu opens.
|
|
3800
|
+
const arranged = rowLayout.arrangeNavInner('more', moreMenuItemsHtml({ key, p, gates }), rowCtx);
|
|
3764
3801
|
if (!/<li\b/.test(arranged)) return '';
|
|
3765
3802
|
// Dividers are not actions — count only what can be clicked.
|
|
3766
3803
|
const n = (arranged.match(/<a /g) || []).length;
|
|
3767
|
-
return
|
|
3768
|
-
|
|
3769
|
-
<
|
|
3770
|
-
|
|
3804
|
+
return menuShellHtml('more', {
|
|
3805
|
+
key, project: p, caption: `${n} action${n === 1 ? '' : 's'}`,
|
|
3806
|
+
html: '<button class="uk-button uk-button-default uk-button-small" type="button" title="More actions"><span uk-icon="icon: more; ratio: .7"></span> More <span uk-icon="icon: chevron-down; ratio: .6"></span></button>',
|
|
3807
|
+
});
|
|
3771
3808
|
})();
|
|
3772
3809
|
// SCREENS is drawn beside the running state in the card's head, so the tool strip leaves it out.
|
|
3773
3810
|
const screensOnTools = '';
|
|
@@ -5783,7 +5820,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
5783
5820
|
// The body of a plugin's row menu that loads when it opens (plugin-loader's rowMenuHtml): the
|
|
5784
5821
|
// plugin's own fragment, arranged like an inline menu's items — the Buttons settings hide, order
|
|
5785
5822
|
// and add to it the same way, and the settings page sees its entries once one has been opened.
|
|
5786
|
-
// The core's own menus (DDEV, COMPOSER, Tests, SCREENS) take the same route with no `body`: their
|
|
5823
|
+
// The core's own menus (DDEV, COMPOSER, Tests, SCREENS, NODE, More) take the same route with no `body`: their
|
|
5787
5824
|
// bodies come from coreRowMenuBody, computed for this one project when the menu opens.
|
|
5788
5825
|
if (pathname.startsWith('/fragments/row-menu/')) {
|
|
5789
5826
|
const m = pathname.match(/^\/fragments\/row-menu\/([a-z0-9-]+)\/([a-z0-9_-]+)\/([a-zA-Z0-9_-]+)$/);
|
|
@@ -168,10 +168,16 @@ test('a drawing that lost shapes to the sanitiser is drawn again; one that never
|
|
|
168
168
|
}
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
-
test('Gemini and Anthropic need a real key; Claude Code draws in a session, retried by its finish step', async () => {
|
|
171
|
+
test('Gemini and Anthropic need a real key; Claude Code draws in a session, retried by its finish step', async (t) => {
|
|
172
172
|
const noKey = await svg.generateSvg({ prompt: 'x', provider: 'anthropic', confirm: 'yes' });
|
|
173
173
|
assert.equal(noKey.status, 503);
|
|
174
174
|
assert.match(noKey.error, /ai_providers\.anthropic\.api_key/);
|
|
175
|
+
// The rest drives Claude Code, which needs the claude CLI on this machine (#113).
|
|
176
|
+
const claude = require('../../images-providers').get('claude_code_cli').available();
|
|
177
|
+
if (!claude.ok) {
|
|
178
|
+
t.skip(`needs Claude Code: ${claude.why}`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
175
181
|
const calls = [];
|
|
176
182
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'svgcc-'));
|
|
177
183
|
try {
|
|
@@ -153,7 +153,11 @@ test('the row shows NODE right after COMPOSER, only where there is a package.jso
|
|
|
153
153
|
assert.strictEqual(DEFAULT_LINES[1][DEFAULT_LINES[1].indexOf('composer') + 1], 'node', 'NODE must come right after COMPOSER by default');
|
|
154
154
|
assert.strictEqual((src.match(/\bnode: nodeMenu\b/g) || []).length, 1, 'handed to the layout once per row');
|
|
155
155
|
const fn = src.slice(src.indexOf('function nodeMenuHtml'), src.indexOf('function openWithMenuHtml'));
|
|
156
|
-
assert.match(fn, /if \(!hasPackageJson\(projectPath\)\) return
|
|
156
|
+
assert.match(fn, /if \(!hasPackageJson\(projectPath\)\) return null;/, 'no package.json, no button');
|
|
157
|
+
// The row carries the button and the caption; the tabs arrive when the menu opens (#111).
|
|
158
|
+
assert.match(fn, /return menuShellHtml\('node', \{/, 'the row draws the lazy shell');
|
|
159
|
+
assert.match(src, /const CORE_ROW_MENUS = new Set\(\[[^\]]*'node'[^\]]*\]\);/, 'the core serves the NODE body itself');
|
|
160
|
+
assert.match(src, /if \(slot === 'node'\) \{\s*const body = nodeMenuBody\(key, project, projectPath, isDdev\);/, 'coreRowMenuBody answers NODE');
|
|
157
161
|
assert.match(fn, /uk-tab="active: \$\{Math\.max\(0, MANAGERS\.indexOf\(info\.preferred\)\)\}"/, 'opens on the manager the project uses');
|
|
158
162
|
assert.match(fn, /info\.locks\[m\] \? 'lock ✓' : 'no lock'/, 'each tab says whether its lock file is there');
|
|
159
163
|
assert.match(fn, /hx-include="closest \.node-package"/);
|
|
@@ -47,7 +47,7 @@ before(async () => {
|
|
|
47
47
|
fs.writeFileSync(path.join(site, 'composer.lock'), '{}');
|
|
48
48
|
fs.writeFileSync(path.join(site, 'package.json'), JSON.stringify({ name: 'site-one', scripts: { 'lint:js': 'eslint .' } }));
|
|
49
49
|
fs.mkdirSync(path.join(root, 'dev/plain-folder'), { recursive: true });
|
|
50
|
-
for (const f of ['cmd-tools-testing.sh', 'cmd-tools-checks.sh', 'cmd-tools-add-users.sh']) fs.writeFileSync(path.join(root, 'dev', f), '#!/usr/bin/env bash\n');
|
|
50
|
+
for (const f of ['cmd-tools-testing.sh', 'cmd-tools-checks.sh', 'cmd-tools-add-users.sh', 'cmd-tools-remove.sh']) fs.writeFileSync(path.join(root, 'dev', f), '#!/usr/bin/env bash\n');
|
|
51
51
|
// A package: no DDEV, a package.json that declares checks.
|
|
52
52
|
const pkg = path.join(root, 'products/pkg-one');
|
|
53
53
|
fs.mkdirSync(pkg, { recursive: true });
|
|
@@ -90,7 +90,7 @@ test('a project row carries the button, the caption and an empty shell for each
|
|
|
90
90
|
const { status, html } = await text('/dev');
|
|
91
91
|
assert.strictEqual(status, 200);
|
|
92
92
|
const row = rowOf(html, 'site-one');
|
|
93
|
-
for (const slot of ['ddev', 'composer', 'tests', 'screens']) {
|
|
93
|
+
for (const slot of ['ddev', 'composer', 'tests', 'screens', 'node', 'more']) {
|
|
94
94
|
const menu = menuOf(row, slot);
|
|
95
95
|
assert.ok(menu, `${slot}: the row has the menu`);
|
|
96
96
|
assert.match(menu, new RegExp(`hx-get="/fragments/row-menu/${slot}/dev/site-one"`), `${slot}: the shell asks the core route for its body`);
|
|
@@ -108,8 +108,12 @@ test('a project row carries the button, the caption and an empty shell for each
|
|
|
108
108
|
assert.match(menuOf(row, 'tests'), /uk-button-secondary/, 'the Tests button as it was');
|
|
109
109
|
assert.match(menuOf(row, 'screens'), /hx-trigger="menu-opened, load, every 30s, screens-changed from:body"/, 'SCREENS still polls: the hand and the agent badge ride on its answer');
|
|
110
110
|
assert.match(menuOf(row, 'screens'), /<span class="act-status uk-text-meta">none<\/span>/);
|
|
111
|
+
// NODE says which lock files are there; More counts what it holds (#111).
|
|
112
|
+
assert.match(menuOf(row, 'node'), /<span class="act-status uk-text-meta">no lock<\/span>/);
|
|
113
|
+
assert.match(menuOf(row, 'node'), /class="act-menu-lazy node-dropdown"/, 'the NODE width class rides on the shell');
|
|
114
|
+
assert.match(menuOf(row, 'more'), /<span class="act-status uk-text-meta">\d+ actions?<\/span>/, 'the More caption counts the actions the body will hold');
|
|
111
115
|
// Nothing of the bodies is on the page.
|
|
112
|
-
for (const inner of ['/actions/ddev-restart', '/actions/ddev-describe', '/actions/composer-install', '/actions/testing-configure', '/actions/checks-plan', 'composer-package', 'checks-sub', 'ddev-nav-columns']) {
|
|
116
|
+
for (const inner of ['/actions/ddev-restart', '/actions/ddev-describe', '/actions/composer-install', '/actions/testing-configure', '/actions/checks-plan', 'composer-package', 'checks-sub', 'ddev-nav-columns', 'node-tabs', 'node-col-commands', 'Scripts in package.json', '/fragments/projects/remove-form/dev/site-one', 'Remove the project']) {
|
|
113
117
|
assert.ok(!row.includes(inner), `${inner} is not on the page before a menu opens`);
|
|
114
118
|
}
|
|
115
119
|
// A folder with no DDEV config has the Configure DDEV button and no DDEV menu; no composer.json, no COMPOSER.
|
|
@@ -174,7 +178,8 @@ test('the Tests body for a package is the checks alone, and a plain folder in a
|
|
|
174
178
|
assert.match(pkg.html, /<span class="checks-alias">css<\/span>/);
|
|
175
179
|
assert.ok(!pkg.html.includes('/actions/testing-configure') && !pkg.html.includes('/actions/add-users'), 'a packages workspace has no suite and no test users');
|
|
176
180
|
const page = (await text('/products')).html;
|
|
177
|
-
|
|
181
|
+
// Nothing on a package row posts with its name any more (NODE's body is served, #111): found by its Tests shell.
|
|
182
|
+
assert.match(menuOf(rowOf(page, 'pkg-one', '/fragments/row-menu/tests/products/pkg-one'), 'tests'), /<span class="act-status uk-text-meta">(2 declared|deps missing)<\/span>/, 'the caption says what the package declares');
|
|
178
183
|
const plain = await text('/fragments/row-menu/tests/dev/plain-folder');
|
|
179
184
|
assert.strictEqual(plain.status, 200);
|
|
180
185
|
assert.match(plain.html, /hx-post="\/actions\/testing-configure"/);
|
|
@@ -197,13 +202,40 @@ test('the SCREENS body lists the sessions (none here), the ways to start one, an
|
|
|
197
202
|
assert.match(legacy.html, /^\s*<li class="uk-nav-header">No screens in this project<\/li>/);
|
|
198
203
|
});
|
|
199
204
|
|
|
205
|
+
test('the NODE body for a site is the header, the yarn and npm tabs and the scripts of package.json (#111)', async () => {
|
|
206
|
+
const { status, html } = await text('/fragments/row-menu/node/dev/site-one');
|
|
207
|
+
assert.strictEqual(status, 200);
|
|
208
|
+
assert.match(html, /^<ul class="uk-nav uk-dropdown-nav">/);
|
|
209
|
+
assert.match(html, /Node · site-one/);
|
|
210
|
+
assert.match(html, /Runs as ddev npm \/ ddev yarn in this project\./);
|
|
211
|
+
assert.match(html, /<ul class="uk-tab node-tabs" data-keep-open uk-tab="active: \d+">/, 'the tabs keep the menu open');
|
|
212
|
+
assert.match(html, /<ul class="uk-switcher">/);
|
|
213
|
+
assert.match(html, /yarn\.lock is not here/);
|
|
214
|
+
assert.match(html, /package-lock\.json is not here/);
|
|
215
|
+
assert.match(html, /hx-include="closest \.node-package"/);
|
|
216
|
+
assert.match(html, /Scripts in package\.json/);
|
|
217
|
+
assert.match(html, /"script":"lint:js"/, 'the script from package.json is offered');
|
|
218
|
+
assert.match(html, /hx-vals='\{"workspace":"dev","projectName":"site-one","manager":"npm"\}'/, 'the items post for this project');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('the More body for a site holds the remove, arranged, and the row counted it (#111)', async () => {
|
|
222
|
+
const { status, html } = await text('/fragments/row-menu/more/dev/site-one');
|
|
223
|
+
assert.strictEqual(status, 200);
|
|
224
|
+
assert.match(html, /^<ul class="uk-nav uk-dropdown-nav">/);
|
|
225
|
+
assert.match(html, /hx-get="\/fragments\/projects\/remove-form\/dev\/site-one"/);
|
|
226
|
+
assert.match(html, /Remove the project…/);
|
|
227
|
+
const page = (await text('/dev')).html;
|
|
228
|
+
const count = Number(/(\d+) actions?/.exec(menuOf(rowOf(page, 'site-one'), 'more'))[1]);
|
|
229
|
+
assert.strictEqual((html.match(/<a /g) || []).length, count, 'the row counts the actions the body holds');
|
|
230
|
+
});
|
|
231
|
+
|
|
200
232
|
test('a project that is not there, a menu the row would not draw, and a slot that is not a menu are 404', async () => {
|
|
201
233
|
for (const [p, why] of [
|
|
202
234
|
['/fragments/row-menu/ddev/dev/no-such-project', 'no such project'],
|
|
203
235
|
['/fragments/row-menu/ddev/dev/plain-folder', 'not a DDEV project'],
|
|
204
236
|
['/fragments/row-menu/composer/dev/plain-folder', 'no composer.json'],
|
|
205
237
|
['/fragments/row-menu/ddev/products/pkg-one', 'a packages workspace has no DDEV'],
|
|
206
|
-
['/fragments/row-menu/
|
|
238
|
+
['/fragments/row-menu/node/dev/plain-folder', 'no package.json'],
|
|
207
239
|
['/fragments/row-menu/ddev/nope/site-one', 'no such workspace'],
|
|
208
240
|
['/fragments/row-menu/ddev/dev/site-one?body=/fragments/p/x/menu', 'a core slot takes no plugin body'],
|
|
209
241
|
['/fragments/row-menu/x-plugin/dev/site-one', 'a plugin slot nobody registered'],
|
|
@@ -226,4 +258,6 @@ test('the Buttons settings page learns the lazy menus\' entries without anyone o
|
|
|
226
258
|
assert.ok(seen.composer && Object.keys(seen.composer.groups).length > 0, 'COMPOSER has groups on record');
|
|
227
259
|
assert.ok(seen.tests && Object.keys(seen.tests.groups).length > 0, 'Tests has groups on record');
|
|
228
260
|
assert.ok(seen.ddev && Object.keys(seen.ddev.groups).length > 0, 'DDEV is on record from the rows themselves');
|
|
261
|
+
assert.ok(seen.node && Object.keys(seen.node.groups).length > 0, 'NODE has groups on record (learned, #111)');
|
|
262
|
+
assert.ok(seen.more && Object.keys(seen.more.groups).length > 0, 'More is on record from the rows themselves');
|
|
229
263
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webship.co/workspace",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Webship Workspace: a container-based development workspace with a web dashboard. The core ships empty; workspace folders, hosting providers, git forges, container runtimes and features are @webship.co/workspace-<name> plugins you add one at a time.",
|
|
5
5
|
"license": "GPL-2.0-or-later",
|
|
6
6
|
"repository": {
|