acrylblends 1.3.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/index.d.ts +8 -5
- package/dist/index.js +97 -38
- package/dist/index.js.map +1 -1
- package/legacy/README.md +1 -1
- package/legacy/agent/46c6eadf9230/src/app.ts +293 -0
- package/legacy/agent/46c6eadf9230/src/routes.ts +163 -0
- package/legacy/layouts.json +26 -0
- package/lib/{client-modules-Bkdr83jO.js → client-modules-C5qiVroz.js} +1 -1
- package/lib/cordis/door.d.ts +27 -6
- package/lib/cordis/door.js +41 -10
- package/lib/node-server/register.js +6 -1
- package/lib/node-server.js +1 -1
- package/lib/{transpile-QUK9b3eG.js → transpile-CRRaixyS.js} +19 -4
- package/package.json +1 -1
- package/plugins/acryl-door/src/cli/acryl.ts +13 -4
- package/plugins/acryl-door/src/host/door.ts +41 -3
- package/plugins/acryl-door/src/host/verbs.ts +3 -3
- package/plugins/acryl-node-server/src/register.ts +10 -1
- package/plugins/acryl-node-server/src/transpile.ts +25 -6
- package/plugins/acryl-page/src/favicon.ts +19 -0
- package/plugins/acryl-page/src/index.ts +18 -13
- package/plugins/agent-panel/client/main.ts +9 -0
- package/plugins/agent-panel/headless.ts +2 -2
- package/plugins/desktop/app-page.ts +151 -0
- package/plugins/desktop/assets/acrylblends-logo.png +0 -0
- package/plugins/desktop/brand.ts +164 -0
- package/plugins/desktop/frame.ts +88 -7
- package/plugins/desktop/git.ts +44 -0
- package/plugins/desktop/host-supervisor.ts +63 -0
- package/plugins/desktop/launch.ts +17 -1
- package/plugins/desktop/main.ts +268 -17
- package/plugins/desktop/preload.cjs +8 -0
- package/plugins/desktop/product.ts +117 -0
- package/plugins/desktop/window.ts +4 -0
- package/plugins/desktop-web/main.ts +3 -2
- package/plugins/desktop-web/plugin.json +1 -1
- package/plugins/extensions/index.ts +64 -13
- package/plugins/instance/archive.ts +395 -0
- package/plugins/instance/bundle-overlay.ts +10 -0
- package/plugins/instance/bundle.ts +201 -0
- package/plugins/instance/home.ts +386 -0
- package/plugins/instance/index.ts +132 -24
- package/plugins/instance/storage.ts +413 -0
- package/plugins/plugin-share/index.ts +99 -1
- package/plugins/plugin-share/publish.ts +17 -3
- package/plugins/plugin-share/pull-check.ts +2 -0
- package/plugins/plugin-share/pull-list.ts +71 -0
- package/plugins/showcase/cards.ts +162 -0
- package/plugins/showcase/index.ts +116 -0
- package/plugins/showcase/plugin.json +11 -0
- package/shared/skills/acryl-remove-plugin/SKILL.md +1 -1
- package/templates/agent/AGENTS.md +4 -0
- package/templates/agent/README.md +1 -1
- package/templates/agent/blend.yaml +3 -0
- package/templates/agent/gitignore +2 -0
- package/templates/agent/src/app.ts +2 -0
- package/templates/agent/src/routes.ts +23 -5
- package/templates/agent/template.json +1 -0
- package/templates/agent/test/app.test.ts +26 -0
- package/templates/blank/README.md +1 -1
- package/templates/blank/gitignore +2 -0
- package/templates/blank/src/routes.ts +9 -5
- package/templates/chat/gitignore +2 -0
- package/templates/chat/src/chat-app.ts +7 -3
- package/templates/chat/src/llm.ts +7 -1
- package/templates/chat/test/chat.test.ts +16 -1
- package/templates/nodejs/gitignore +2 -0
- package/templates/notes/gitignore +2 -0
- package/templates/todo/README.md +1 -1
- package/templates/todo/gitignore +2 -0
package/lib/cordis/door.js
CHANGED
|
@@ -2,7 +2,7 @@ import { t as Acryl } from "../app-CQcGWjrk.js";
|
|
|
2
2
|
import { r as streamSSE } from "../streaming-BlDaph8U.js";
|
|
3
3
|
import { r as loopbackAuthority } from "../loopback-only-BbasoF7R.js";
|
|
4
4
|
import "../helpers-BEjcdbgw.js";
|
|
5
|
-
import { n as clientModules } from "../client-modules-
|
|
5
|
+
import { n as clientModules } from "../client-modules-C5qiVroz.js";
|
|
6
6
|
import { a as MAX_WAIT_MS, d as parseChannelToHost, l as UiControlError, p as parseUiRequest, r as MAX_SNAPSHOT_NODES, u as WINDOW_ID } from "../protocol-CQ_if7Dg.js";
|
|
7
7
|
import { a as INVALID_JSON, c as operatorOnly, d as AuditLog, f as auditPath, i as DOOR_PREFIX, l as privateApi, o as error, p as UiChannel, r as pageChannelRoutes, s as jsonBody, t as renderSnapshot, u as readJson } from "../snapshot-text-CXTnjxGx.js";
|
|
8
8
|
import "../src-iBJ2ysV9.js";
|
|
@@ -464,15 +464,20 @@ const VERB_SPECS = {
|
|
|
464
464
|
}
|
|
465
465
|
},
|
|
466
466
|
"plugin-pull": {
|
|
467
|
-
usage: "plugin pull <source>: fetch a plugin someone released (github:<owner>/<repo>[@<version>][@sha256:<digest>], or a .tgz), check
|
|
468
|
-
description: "Fetch a plugin someone released, without running anything; check its digest, that it works on every OS and that this app has the services it needs; then install and start it without a restart. A refusal says why and changes nothing.",
|
|
467
|
+
usage: "plugin pull <source> | --from <list>: fetch a plugin someone released (github:<owner>/<repo>[@<version>][@sha256:<digest>], or a .tgz), or every plugin of a group's list, check each and start it, live",
|
|
468
|
+
description: "Fetch a plugin someone released, without running anything; check its digest, that it works on every OS and that this app has the services it needs; then install and start it without a restart. A refusal says why and changes nothing. With from instead of source, pull every source of a group's list (one per line, # comments); each is pulled on its own, so a refused one is named with its reason and the others still come in.",
|
|
469
469
|
arguments: {
|
|
470
470
|
type: "object",
|
|
471
|
-
properties: {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
471
|
+
properties: {
|
|
472
|
+
source: {
|
|
473
|
+
type: "string",
|
|
474
|
+
description: "github:<owner>/<repo>[@<version>][@sha256:<digest>], or a .tgz path or https address"
|
|
475
|
+
},
|
|
476
|
+
from: {
|
|
477
|
+
type: "string",
|
|
478
|
+
description: "instead of source: a list of sources, a file in the app (e.g. group.txt) or an https address"
|
|
479
|
+
}
|
|
480
|
+
},
|
|
476
481
|
additionalProperties: false
|
|
477
482
|
}
|
|
478
483
|
},
|
|
@@ -708,7 +713,7 @@ var Door = class {
|
|
|
708
713
|
}
|
|
709
714
|
case "plugin-list": return this.plugin((plugins) => plugins.list(), false, this.installed());
|
|
710
715
|
case "plugin-add": return this.plugin((extensions) => extensions.install(this.insideApp(input.folder)));
|
|
711
|
-
case "plugin-pull": return this.pull(input.source);
|
|
716
|
+
case "plugin-pull": return input.from === void 0 ? this.pull(input.source) : this.pullList(input.from, input.source);
|
|
712
717
|
case "plugin-publish": return this.planPublish(input);
|
|
713
718
|
case "plugin-remove": {
|
|
714
719
|
const name = input.name;
|
|
@@ -997,13 +1002,29 @@ var Door = class {
|
|
|
997
1002
|
async pull(source) {
|
|
998
1003
|
if (typeof source !== "string" || source === "") throw new DoorError("invalid", "plugin pull needs a source, e.g. plugin pull github:someone/pomodoro@0.1.0");
|
|
999
1004
|
const value = await this.share().pull(source);
|
|
1000
|
-
if (!value.ok) throw new DoorError("refused", `${value.reason ?? "the pull was refused"}
|
|
1005
|
+
if (!value.ok) throw new DoorError("refused", `${sentence(value.reason ?? "the pull was refused")} ${sentence(value.next ?? "nothing changed")}`);
|
|
1001
1006
|
if (value.action !== "unchanged" && this.deps.reloadOnChange !== false) await this.deps.channel.reloadAll();
|
|
1002
1007
|
return {
|
|
1003
1008
|
value,
|
|
1004
1009
|
text: JSON.stringify(value, null, 2)
|
|
1005
1010
|
};
|
|
1006
1011
|
}
|
|
1012
|
+
/** A group's list: every source is pulled on its own, so one refused plugin is named with its reason and the others still install and run. */
|
|
1013
|
+
async pullList(from, source) {
|
|
1014
|
+
if (source !== void 0) throw new DoorError("invalid", "plugin pull takes a source or --from <list>, not both");
|
|
1015
|
+
if (typeof from !== "string" || from === "") throw new DoorError("invalid", "plugin pull --from needs a list, e.g. plugin pull --from group.txt");
|
|
1016
|
+
const value = await this.share().pullList(from);
|
|
1017
|
+
if (!value.ok) throw new DoorError("refused", `${sentence(value.reason)} ${sentence(value.next)}`);
|
|
1018
|
+
if (value.entries.some((entry) => entry.result.ok && entry.result.action !== "unchanged") && this.deps.reloadOnChange !== false) await this.deps.channel.reloadAll();
|
|
1019
|
+
const lines = value.entries.map((entry) => `${entry.source}: ${pullLine(entry.result)}`);
|
|
1020
|
+
const refused = value.entries.filter((entry) => !entry.result.ok);
|
|
1021
|
+
const summary = `${String(value.entries.length - refused.length)} of ${String(value.entries.length)} from ${value.from} pulled`;
|
|
1022
|
+
if (refused.length > 0) throw new DoorError("refused", [`${summary}; ${String(refused.length)} refused, the others are in and running.`, ...lines].join("\n"));
|
|
1023
|
+
return {
|
|
1024
|
+
value,
|
|
1025
|
+
text: [`${summary}.`, ...lines].join("\n")
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1007
1028
|
/** A folder named by an agent, resolved from the app folder; it must be inside the app. */
|
|
1008
1029
|
insideApp(folder) {
|
|
1009
1030
|
if (typeof folder !== "string" || folder === "") throw new DoorError("invalid", "plugin add needs a folder, e.g. plugins/pomodoro");
|
|
@@ -1040,6 +1061,16 @@ var Door = class {
|
|
|
1040
1061
|
};
|
|
1041
1062
|
}
|
|
1042
1063
|
};
|
|
1064
|
+
/** One source's outcome in a list pull, as a person reads it. */
|
|
1065
|
+
function pullLine(result) {
|
|
1066
|
+
if (!result.ok) return `refused: ${sentence(result.reason ?? "the pull was refused")} ${sentence(result.next ?? "nothing changed")}`;
|
|
1067
|
+
return `${result.action ?? "pulled"}${result.name === void 0 ? "" : ` ${result.name}`}`;
|
|
1068
|
+
}
|
|
1069
|
+
/** A text as one sentence: the full stop added only when it has no closing punctuation of its own (a reason may end with one, a next step often does). */
|
|
1070
|
+
function sentence(text) {
|
|
1071
|
+
const trimmed = text.trim();
|
|
1072
|
+
return /[.!?]$/u.test(trimmed) ? trimmed : `${trimmed}.`;
|
|
1073
|
+
}
|
|
1043
1074
|
//#endregion
|
|
1044
1075
|
//#region ../../plugins/acryl-door/src/host/chat-tools.ts
|
|
1045
1076
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as compileModule } from "../transpile-
|
|
1
|
+
import { n as compileModule, r as usePackagesOfProgram } from "../transpile-CRRaixyS.js";
|
|
2
2
|
import { registerHooks } from "node:module";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -35,6 +35,11 @@ registerHooks({ load(url, context, nextLoad) {
|
|
|
35
35
|
*/
|
|
36
36
|
const plugins = new URL("../../", import.meta.url);
|
|
37
37
|
const appRoot = new URL("../", plugins).href;
|
|
38
|
+
const family = new URL("instance/bundle.ts", plugins);
|
|
39
|
+
if (existsSync(fileURLToPath(family))) {
|
|
40
|
+
const { appTree } = await import(family.href);
|
|
41
|
+
if (appTree(process.env, fileURLToPath(appRoot)) !== void 0) usePackagesOfProgram(fileURLToPath(appRoot));
|
|
42
|
+
}
|
|
38
43
|
const resolver = new URL("extensions/resolve.ts", plugins);
|
|
39
44
|
if (existsSync(fileURLToPath(new URL("acryl-router/src/index.ts", plugins))) && existsSync(fileURLToPath(resolver))) {
|
|
40
45
|
const { routerAliases } = await import(resolver.href);
|
package/lib/node-server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as serve, d as RequestError, i as createAdaptorServer, n as UpgradeEmitter, o as upgradeWebSocket, r as UpgradeHub, s as getRequestListener, t as testWebSocket } from "./test-websocket-CQOSvBth.js";
|
|
2
2
|
import { t as serveStatic } from "./serve-static-C1Nkj_xB.js";
|
|
3
3
|
import { t as getConnInfo } from "./conninfo-CWJwieh4.js";
|
|
4
|
-
import { n as clientModules, t as buildClientModules } from "./client-modules-
|
|
4
|
+
import { n as clientModules, t as buildClientModules } from "./client-modules-C5qiVroz.js";
|
|
5
5
|
export { RequestError, UpgradeEmitter, UpgradeHub, buildClientModules, clientModules, createAdaptorServer, getConnInfo, getRequestListener, serve, serveStatic, testWebSocket, upgradeWebSocket };
|
|
@@ -23,10 +23,25 @@ const importSources = /* @__PURE__ */ new Map();
|
|
|
23
23
|
* @returns the compiler.
|
|
24
24
|
* @throws MissingTypeScriptError when the app has no `typescript`.
|
|
25
25
|
*/
|
|
26
|
+
/** The nearest folder from `start` up whose node_modules has `typescript`, or undefined. */
|
|
27
|
+
function typescriptOwner(start) {
|
|
28
|
+
for (let dir = start;; dir = dirname(dir)) {
|
|
29
|
+
if (existsSync(join(dir, "node_modules", "typescript", "package.json"))) return dir;
|
|
30
|
+
if (dirname(dir) === dir) return void 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** The Program's folder when this process is a desktop product's frame host: the person's source has no packages, the bundle has them. */
|
|
34
|
+
let programPackages;
|
|
35
|
+
/**
|
|
36
|
+
* In a desktop product's frame host, compile with the Program's packages when a file's own chain has none (`register.ts` calls this once it finds the
|
|
37
|
+
* product's app tree, plugins/instance/bundle.ts). Nowhere else: an app without `typescript` is told so.
|
|
38
|
+
*/
|
|
39
|
+
function usePackagesOfProgram(dir) {
|
|
40
|
+
programPackages = dir;
|
|
41
|
+
}
|
|
26
42
|
function compilerFor(file, what) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
else if (dirname(dir) === dir) throw new MissingTypeScriptError(file, what);
|
|
43
|
+
const owner = typescriptOwner(dirname(file)) ?? (programPackages === void 0 ? void 0 : typescriptOwner(programPackages));
|
|
44
|
+
if (owner === void 0) throw new MissingTypeScriptError(file, what);
|
|
30
45
|
const path = createRequire(join(owner, "package.json")).resolve("typescript");
|
|
31
46
|
let compiler = compilers.get(path);
|
|
32
47
|
if (compiler === void 0) {
|
|
@@ -87,4 +102,4 @@ function rewriteSpecifiers(ts, code, rewrite) {
|
|
|
87
102
|
return result + code.slice(last);
|
|
88
103
|
}
|
|
89
104
|
//#endregion
|
|
90
|
-
export { compileModule as n, MissingTypeScriptError as t };
|
|
105
|
+
export { compileModule as n, usePackagesOfProgram as r, MissingTypeScriptError as t };
|
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
17
17
|
import { readDoorFile, STATE_DIR, DOOR_FILE } from '../host/door-file.ts'
|
|
18
18
|
|
|
19
19
|
const ACT_OPS = new Set(['click', 'type', 'select', 'press', 'scroll', 'wait', 'reload', 'navigate'])
|
|
20
|
-
const VALUED = new Set(['--dir', '--owner', '--license', '--ref', '--text', '--option', '--key', '--direction', '--amount', '--cursor', '--max-nodes', '--role', '--name', '--timeout-ms', '--id', '--width', '--height', '--window', '--tool', '--args', '--path'])
|
|
20
|
+
const VALUED = new Set(['--dir', '--owner', '--license', '--ref', '--text', '--option', '--key', '--direction', '--amount', '--cursor', '--max-nodes', '--role', '--name', '--timeout-ms', '--id', '--width', '--height', '--window', '--tool', '--args', '--path', '--from'])
|
|
21
21
|
const FLAGS = new Set(['--json', '--submit', '--no-clear', '--gone', '--force', '--help'])
|
|
22
22
|
|
|
23
23
|
export class UsageError extends Error {}
|
|
@@ -139,7 +139,12 @@ export function parseInvocation(argv: readonly string[], env: NodeJS.ProcessEnv
|
|
|
139
139
|
case 'plugin': {
|
|
140
140
|
if (second === 'list' || second === 'reload') return make(`plugin-${second}`)
|
|
141
141
|
if (second === 'add') { if (third === undefined) throw new UsageError('plugin add needs a folder, e.g. plugin add plugins/pomodoro'); return make('plugin-add', { folder: third }) }
|
|
142
|
-
if (second === 'pull') {
|
|
142
|
+
if (second === 'pull') {
|
|
143
|
+
const from = values.get('--from')
|
|
144
|
+
if (from !== undefined) { if (third !== undefined) throw new UsageError('plugin pull takes a source or --from <list>, not both'); return make('plugin-pull', { from }) }
|
|
145
|
+
if (third === undefined) throw new UsageError('plugin pull needs a source, e.g. plugin pull github:someone/pomodoro@0.1.0, or --from <list>')
|
|
146
|
+
return make('plugin-pull', { source: third })
|
|
147
|
+
}
|
|
143
148
|
if (second === 'publish') {
|
|
144
149
|
if (third === undefined) throw new UsageError('plugin publish needs a plugin name, e.g. plugin publish pomodoro')
|
|
145
150
|
const owner = values.get('--owner')
|
|
@@ -191,13 +196,17 @@ export function parseInvocation(argv: readonly string[], env: NodeJS.ProcessEnv
|
|
|
191
196
|
}
|
|
192
197
|
}
|
|
193
198
|
|
|
194
|
-
/**
|
|
199
|
+
/**
|
|
200
|
+
* The app folder: `--dir`, or the nearest folder upward from `cwd` that has `.acryl/door.json`. When no app is running, the nearest folder that is an app (it
|
|
201
|
+
* has a `blend.yaml`), so "no running app in <that folder>" names the app. A folder that only has a `.acryl/` is not an app: the person's `~/.acryl` is ACRYL's
|
|
202
|
+
* own home, and it must never be taken for the app the command was run in.
|
|
203
|
+
*/
|
|
195
204
|
export function findApp(cwd: string, dir: string | undefined): string | undefined {
|
|
196
205
|
if (dir !== undefined) return resolve(cwd, dir)
|
|
197
206
|
let fallback: string | undefined
|
|
198
207
|
for (let at = resolve(cwd); ; at = dirname(at)) {
|
|
199
208
|
if (existsSync(join(at, STATE_DIR, DOOR_FILE))) return at
|
|
200
|
-
if (fallback === undefined && existsSync(join(at,
|
|
209
|
+
if (fallback === undefined && existsSync(join(at, 'blend.yaml'))) fallback = at
|
|
201
210
|
if (dirname(at) === at) return fallback
|
|
202
211
|
}
|
|
203
212
|
}
|
|
@@ -41,11 +41,21 @@ export interface PluginSharePort {
|
|
|
41
41
|
list(): readonly unknown[]
|
|
42
42
|
/** Take a plugin out and forget its record; a pulled folder still holding the release as pulled goes too. */
|
|
43
43
|
remove(name: string): Promise<unknown>
|
|
44
|
-
pull(source: string): Promise<
|
|
44
|
+
pull(source: string): Promise<PullView>
|
|
45
|
+
/** Pull each source of a group's list on its own: one that is refused does not stop the others. */
|
|
46
|
+
pullList(from: string): Promise<PullListView>
|
|
45
47
|
planPublish(name: string, options?: PublishOptionsView): Promise<PublishPlanView | { readonly ok: false; readonly reason: string; readonly next: string }>
|
|
46
48
|
publish(name: string, confirmed: PublishPlanView, options?: PublishOptionsView): Promise<{ readonly ok: boolean; readonly reason?: string }>
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
/** What one pull did, or why it was refused. */
|
|
52
|
+
export interface PullView { readonly ok: boolean; readonly action?: string; readonly name?: string; readonly reason?: string; readonly next?: string }
|
|
53
|
+
|
|
54
|
+
/** What a list pull did for each source, or why the list itself was refused (then nothing was pulled). */
|
|
55
|
+
export type PullListView =
|
|
56
|
+
| { readonly ok: true; readonly from: string; readonly entries: readonly { readonly line: number; readonly source: string; readonly result: PullView }[] }
|
|
57
|
+
| { readonly ok: false; readonly reason: string; readonly next: string }
|
|
58
|
+
|
|
49
59
|
/** Who a plan publishes as, and under which license; the service's defaults when absent. */
|
|
50
60
|
export interface PublishOptionsView {
|
|
51
61
|
readonly owner?: string
|
|
@@ -159,7 +169,7 @@ export class Door {
|
|
|
159
169
|
}
|
|
160
170
|
case 'plugin-list': return this.plugin(plugins => plugins.list(), false, this.installed())
|
|
161
171
|
case 'plugin-add': return this.plugin(extensions => extensions.install(this.insideApp(input.folder)))
|
|
162
|
-
case 'plugin-pull': return this.pull(input.source)
|
|
172
|
+
case 'plugin-pull': return input.from === undefined ? this.pull(input.source) : this.pullList(input.from, input.source)
|
|
163
173
|
case 'plugin-publish': return this.planPublish(input)
|
|
164
174
|
case 'plugin-remove': {
|
|
165
175
|
const name = input.name
|
|
@@ -388,12 +398,28 @@ export class Door {
|
|
|
388
398
|
if (typeof source !== 'string' || source === '') throw new DoorError('invalid', 'plugin pull needs a source, e.g. plugin pull github:someone/pomodoro@0.1.0')
|
|
389
399
|
const value = await this.share().pull(source)
|
|
390
400
|
// A refusal is a failed command, so a script does not read it as done; it changed nothing, and the open pages stay.
|
|
391
|
-
if (!value.ok) throw new DoorError('refused', `${value.reason ?? 'the pull was refused'}
|
|
401
|
+
if (!value.ok) throw new DoorError('refused', `${sentence(value.reason ?? 'the pull was refused')} ${sentence(value.next ?? 'nothing changed')}`)
|
|
392
402
|
// Neither did pulling what is already there change anything.
|
|
393
403
|
if (value.action !== 'unchanged' && this.deps.reloadOnChange !== false) await this.deps.channel.reloadAll()
|
|
394
404
|
return { value, text: JSON.stringify(value, null, 2) }
|
|
395
405
|
}
|
|
396
406
|
|
|
407
|
+
/** A group's list: every source is pulled on its own, so one refused plugin is named with its reason and the others still install and run. */
|
|
408
|
+
private async pullList(from: unknown, source: unknown): Promise<VerbAnswer> {
|
|
409
|
+
if (source !== undefined) throw new DoorError('invalid', 'plugin pull takes a source or --from <list>, not both')
|
|
410
|
+
if (typeof from !== 'string' || from === '') throw new DoorError('invalid', 'plugin pull --from needs a list, e.g. plugin pull --from group.txt')
|
|
411
|
+
const value = await this.share().pullList(from)
|
|
412
|
+
if (!value.ok) throw new DoorError('refused', `${sentence(value.reason)} ${sentence(value.next)}`)
|
|
413
|
+
const changed = value.entries.some(entry => entry.result.ok && entry.result.action !== 'unchanged')
|
|
414
|
+
if (changed && this.deps.reloadOnChange !== false) await this.deps.channel.reloadAll()
|
|
415
|
+
const lines = value.entries.map(entry => `${entry.source}: ${pullLine(entry.result)}`)
|
|
416
|
+
const refused = value.entries.filter(entry => !entry.result.ok)
|
|
417
|
+
const summary = `${String(value.entries.length - refused.length)} of ${String(value.entries.length)} from ${value.from} pulled`
|
|
418
|
+
// Some refused: the command failed, so a script sees it, but the ones that came in stay in and keep running.
|
|
419
|
+
if (refused.length > 0) throw new DoorError('refused', [`${summary}; ${String(refused.length)} refused, the others are in and running.`, ...lines].join('\n'))
|
|
420
|
+
return { value, text: [`${summary}.`, ...lines].join('\n') }
|
|
421
|
+
}
|
|
422
|
+
|
|
397
423
|
/** A folder named by an agent, resolved from the app folder; it must be inside the app. */
|
|
398
424
|
private insideApp(folder: unknown): string {
|
|
399
425
|
if (typeof folder !== 'string' || folder === '') throw new DoorError('invalid', 'plugin add needs a folder, e.g. plugins/pomodoro')
|
|
@@ -426,3 +452,15 @@ export class Door {
|
|
|
426
452
|
return { value, text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }
|
|
427
453
|
}
|
|
428
454
|
}
|
|
455
|
+
|
|
456
|
+
/** One source's outcome in a list pull, as a person reads it. */
|
|
457
|
+
function pullLine(result: PullView): string {
|
|
458
|
+
if (!result.ok) return `refused: ${sentence(result.reason ?? 'the pull was refused')} ${sentence(result.next ?? 'nothing changed')}`
|
|
459
|
+
return `${result.action ?? 'pulled'}${result.name === undefined ? '' : ` ${result.name}`}`
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** A text as one sentence: the full stop added only when it has no closing punctuation of its own (a reason may end with one, a next step often does). */
|
|
463
|
+
function sentence(text: string): string {
|
|
464
|
+
const trimmed = text.trim()
|
|
465
|
+
return /[.!?]$/u.test(trimmed) ? trimmed : `${trimmed}.`
|
|
466
|
+
}
|
|
@@ -87,9 +87,9 @@ export const VERB_SPECS: Readonly<Record<Verb, VerbSpec>> = {
|
|
|
87
87
|
arguments: { type: 'object', properties: { folder: { type: 'string', description: 'a folder inside the app, e.g. plugins/pomodoro' } }, required: ['folder'], additionalProperties: false },
|
|
88
88
|
},
|
|
89
89
|
'plugin-pull': {
|
|
90
|
-
usage: 'plugin pull <source>: fetch a plugin someone released (github:<owner>/<repo>[@<version>][@sha256:<digest>], or a .tgz), check
|
|
91
|
-
description: 'Fetch a plugin someone released, without running anything; check its digest, that it works on every OS and that this app has the services it needs; then install and start it without a restart. A refusal says why and changes nothing.',
|
|
92
|
-
arguments: { type: 'object', properties: { source: { type: 'string', description: 'github:<owner>/<repo>[@<version>][@sha256:<digest>], or a .tgz path or https address' }
|
|
90
|
+
usage: 'plugin pull <source> | --from <list>: fetch a plugin someone released (github:<owner>/<repo>[@<version>][@sha256:<digest>], or a .tgz), or every plugin of a group\'s list, check each and start it, live',
|
|
91
|
+
description: 'Fetch a plugin someone released, without running anything; check its digest, that it works on every OS and that this app has the services it needs; then install and start it without a restart. A refusal says why and changes nothing. With from instead of source, pull every source of a group\'s list (one per line, # comments); each is pulled on its own, so a refused one is named with its reason and the others still come in.',
|
|
92
|
+
arguments: { type: 'object', properties: { source: { type: 'string', description: 'github:<owner>/<repo>[@<version>][@sha256:<digest>], or a .tgz path or https address' }, from: { type: 'string', description: 'instead of source: a list of sources, a file in the app (e.g. group.txt) or an https address' } }, additionalProperties: false },
|
|
93
93
|
},
|
|
94
94
|
'plugin-publish': {
|
|
95
95
|
usage: 'plugin publish <name> [--owner <account>] [--license <id>]: show what publishing this plugin would send; in a person\'s own terminal, ask them',
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { existsSync, readFileSync } from 'node:fs'
|
|
11
11
|
import { registerHooks } from 'node:module'
|
|
12
12
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
13
|
-
import { compileModule } from './transpile.ts'
|
|
13
|
+
import { compileModule, usePackagesOfProgram } from './transpile.ts'
|
|
14
14
|
|
|
15
15
|
/** The JSX runtime the docs set up; an app's tsconfig.json (or a file's own `@jsxImportSource` pragma) chooses another. */
|
|
16
16
|
const DEFAULT_IMPORT_SOURCE = 'acrylblends/jsx'
|
|
@@ -36,6 +36,15 @@ registerHooks({
|
|
|
36
36
|
*/
|
|
37
37
|
const plugins = new URL('../../', import.meta.url)
|
|
38
38
|
const appRoot = new URL('../', plugins).href
|
|
39
|
+
// A desktop product's frame host runs the Program from the bundle on an app's source, which holds only the person's files: their JSX compiles with the
|
|
40
|
+
// Program's `typescript` (the instance family says whether this is such a host; a published framework has no family beside it).
|
|
41
|
+
const family = new URL('instance/bundle.ts', plugins)
|
|
42
|
+
if (existsSync(fileURLToPath(family))) {
|
|
43
|
+
const { appTree } = (await import(family.href)) as {
|
|
44
|
+
appTree(env: NodeJS.ProcessEnv, bundle: string): unknown
|
|
45
|
+
}
|
|
46
|
+
if (appTree(process.env, fileURLToPath(appRoot)) !== undefined) usePackagesOfProgram(fileURLToPath(appRoot))
|
|
47
|
+
}
|
|
39
48
|
const resolver = new URL('extensions/resolve.ts', plugins)
|
|
40
49
|
if (existsSync(fileURLToPath(new URL('acryl-router/src/index.ts', plugins))) && existsSync(fileURLToPath(resolver))) {
|
|
41
50
|
const { routerAliases } = (await import(resolver.href)) as { routerAliases(): Record<string, string> }
|
|
@@ -36,13 +36,32 @@ const importSources = new Map<string, string>()
|
|
|
36
36
|
* @returns the compiler.
|
|
37
37
|
* @throws MissingTypeScriptError when the app has no `typescript`.
|
|
38
38
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
let
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
else if (dirname(dir) === dir) throw new MissingTypeScriptError(file, what)
|
|
39
|
+
/** The nearest folder from `start` up whose node_modules has `typescript`, or undefined. */
|
|
40
|
+
function typescriptOwner(start: string): string | undefined {
|
|
41
|
+
for (let dir = start; ; dir = dirname(dir)) {
|
|
42
|
+
if (existsSync(join(dir, 'node_modules', 'typescript', 'package.json'))) return dir
|
|
43
|
+
if (dirname(dir) === dir) return undefined
|
|
45
44
|
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The Program's folder when this process is a desktop product's frame host: the person's source has no packages, the bundle has them. */
|
|
48
|
+
let programPackages: string | undefined
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* In a desktop product's frame host, compile with the Program's packages when a file's own chain has none (`register.ts` calls this once it finds the
|
|
52
|
+
* product's app tree, plugins/instance/bundle.ts). Nowhere else: an app without `typescript` is told so.
|
|
53
|
+
*/
|
|
54
|
+
export function usePackagesOfProgram(dir: string): void {
|
|
55
|
+
programPackages = dir
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function compilerFor(file: string, what: string): Compiler {
|
|
59
|
+
// Only the app's own node_modules chain counts: Node's global folders (NODE_PATH, which a package manager's shims may set) are not the app. In a
|
|
60
|
+
// desktop product the app is the person's source with the Program's packages.
|
|
61
|
+
const owner =
|
|
62
|
+
typescriptOwner(dirname(file)) ??
|
|
63
|
+
(programPackages === undefined ? undefined : typescriptOwner(programPackages))
|
|
64
|
+
if (owner === undefined) throw new MissingTypeScriptError(file, what)
|
|
46
65
|
const path = createRequire(join(owner, 'package.json')).resolve('typescript')
|
|
47
66
|
let compiler = compilers.get(path)
|
|
48
67
|
if (compiler === undefined) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page's own icon: the ACRYL Blends logo (plugins/desktop/assets/acrylblends-logo.png at 64 px) inside an SVG, which is what a browser tab reads at 16 px. The wordmark is
|
|
3
|
+
* unreadable that small. The frontend package the page borrows its scripts from ships DeepSeek's whale as favicon.svg, so the page answers every icon request itself and
|
|
4
|
+
* never serves that file.
|
|
5
|
+
*/
|
|
6
|
+
export const ACRYL_FAVICON_SVG =
|
|
7
|
+
"<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 64 64\" role=\"img\" aria-label=\"ACRYL Blends\"><image width=\"64\" height=\"64\" xlink:href=\"data:image/png;base64,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\"/></svg>"
|
|
8
|
+
|
|
9
|
+
/** The web manifest of an app page: its title and the icon above, not the frontend package's own. */
|
|
10
|
+
export function pageManifest(title: string): string {
|
|
11
|
+
return JSON.stringify({
|
|
12
|
+
name: title,
|
|
13
|
+
short_name: title,
|
|
14
|
+
start_url: './',
|
|
15
|
+
scope: './',
|
|
16
|
+
display: 'standalone',
|
|
17
|
+
icons: [{ src: 'favicon.svg', sizes: 'any', type: 'image/svg+xml', purpose: 'any' }],
|
|
18
|
+
})
|
|
19
|
+
}
|
|
@@ -10,6 +10,7 @@ import { dirname, extname, join, normalize, sep } from 'node:path'
|
|
|
10
10
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
11
11
|
import type { Context } from '@deepseek-ai/cordis'
|
|
12
12
|
import type {} from 'acryl-web-server'
|
|
13
|
+
import { ACRYL_FAVICON_SVG, pageManifest } from './favicon.ts'
|
|
13
14
|
|
|
14
15
|
/** Stable Cordis plugin name, equal to the package name and the Loader row id. */
|
|
15
16
|
export const name = 'acryl-page'
|
|
@@ -64,12 +65,17 @@ export function clientAssetTags(indexHtml: string): string {
|
|
|
64
65
|
|
|
65
66
|
/** Our document: minimal on purpose; the frame renders into `#root`, and the taps add the boot wire before `</head>`. */
|
|
66
67
|
export function pageDocument(tags: string, title: string, taps: (html: string) => string): string {
|
|
68
|
+
// The title is an app's name, typed by a person: markup in it is text, never the page's.
|
|
69
|
+
const text = title.replace(/[&<>"']/gu, (character) => `&#${String(character.charCodeAt(0))};`)
|
|
67
70
|
const document = `<!doctype html>
|
|
68
71
|
<html lang="en">
|
|
69
72
|
<head>
|
|
70
73
|
<meta charset="utf-8" />
|
|
71
74
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
72
|
-
<title>${
|
|
75
|
+
<title>${text}</title>
|
|
76
|
+
<meta name="application-name" content="${text}" />
|
|
77
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
78
|
+
<link rel="manifest" href="/manifest.webmanifest" />
|
|
73
79
|
${tags}
|
|
74
80
|
</head>
|
|
75
81
|
<body>
|
|
@@ -143,21 +149,20 @@ export function apply(ctx: Context, config: Partial<Config> = {}): void {
|
|
|
143
149
|
await serveFile(assets, pathname.slice('/assets'.length), res, req.method ?? 'GET')
|
|
144
150
|
},
|
|
145
151
|
})
|
|
146
|
-
// Browsers ask for /favicon.ico unconditionally
|
|
152
|
+
// Browsers ask for /favicon.ico unconditionally. The icon is ours (the ACRYL mark), never the frontend package's DeepSeek whale.
|
|
153
|
+
const sendIcon = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
|
|
154
|
+
res.writeHead(200, { 'content-type': 'image/svg+xml', 'content-length': String(Buffer.byteLength(ACRYL_FAVICON_SVG)), 'cache-control': 'no-cache' })
|
|
155
|
+
res.end(req.method === 'HEAD' ? undefined : ACRYL_FAVICON_SVG)
|
|
156
|
+
}
|
|
157
|
+
for (const path of ['/favicon.ico', '/favicon.svg', '/favicon-dark.svg'])
|
|
158
|
+
ctx.webServer.register({ kind: 'exact', path, handler: sendIcon })
|
|
147
159
|
ctx.webServer.register({
|
|
148
160
|
kind: 'exact',
|
|
149
|
-
path: '/
|
|
161
|
+
path: '/manifest.webmanifest',
|
|
150
162
|
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
|
151
|
-
|
|
163
|
+
const manifest = pageManifest(title)
|
|
164
|
+
res.writeHead(200, { 'content-type': 'application/manifest+json', 'content-length': String(Buffer.byteLength(manifest)) })
|
|
165
|
+
res.end(req.method === 'HEAD' ? undefined : manifest)
|
|
152
166
|
},
|
|
153
167
|
})
|
|
154
|
-
for (const file of ['favicon.svg', 'favicon-dark.svg', 'manifest.webmanifest']) {
|
|
155
|
-
ctx.webServer.register({
|
|
156
|
-
kind: 'exact',
|
|
157
|
-
path: `/${file}`,
|
|
158
|
-
handler: async (req: IncomingMessage, res: ServerResponse) => {
|
|
159
|
-
await serveFile(dist, file, res, req.method ?? 'GET')
|
|
160
|
-
},
|
|
161
|
-
})
|
|
162
|
-
}
|
|
163
168
|
}
|
|
@@ -525,6 +525,8 @@ export async function start(prefix: string = '/api/agent-panel'): Promise<void>
|
|
|
525
525
|
const watch = async (source: RunSource, own: boolean): Promise<void> => {
|
|
526
526
|
let live = true
|
|
527
527
|
let changed = false
|
|
528
|
+
// Every run the server finishes ends with `done` or `error`; a stream that stops without either lost its server, and the run with it.
|
|
529
|
+
let finished = false
|
|
528
530
|
let answer: HTMLElement | undefined
|
|
529
531
|
let answered = ''
|
|
530
532
|
// The Assistant's steps on the page, shown under its answer in one line, as the person reads what it did.
|
|
@@ -592,11 +594,18 @@ export async function start(prefix: string = '/api/agent-panel'): Promise<void>
|
|
|
592
594
|
changed = true
|
|
593
595
|
line(`plugin ${String(data.extension)}: ${String(data.action)}`, 'tool')
|
|
594
596
|
} else if (event === 'error') {
|
|
597
|
+
finished = true
|
|
595
598
|
line(String(data.message ?? 'The run failed.'), 'err')
|
|
596
599
|
} else if (event === 'done') {
|
|
600
|
+
finished = true
|
|
597
601
|
changed = changed || data.changed === true
|
|
598
602
|
}
|
|
599
603
|
}
|
|
604
|
+
if (live && working !== undefined && !finished)
|
|
605
|
+
line(
|
|
606
|
+
'The app stopped while the agent was working, and the run ended with it (`npm run dev` restarts the app when a file it runs changes). Ask again to go on.',
|
|
607
|
+
'err',
|
|
608
|
+
)
|
|
600
609
|
} catch (error) {
|
|
601
610
|
line(`The request failed: ${error instanceof Error ? error.message : String(error)}`, 'err')
|
|
602
611
|
} finally {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/** The panel's own drivers, all `chat`: the app's agent, and the coding agents (`claude`, `codex`, `pi`) started headless in the app folder. */
|
|
2
2
|
import { type ChildProcess, spawn, spawnSync } from 'node:child_process'
|
|
3
3
|
import { readFileSync } from 'node:fs'
|
|
4
|
-
import { homedir } from 'node:os'
|
|
5
4
|
import { join } from 'node:path'
|
|
6
5
|
import type { Context } from '@deepseek-ai/cordis'
|
|
6
|
+
import { personHome } from '../instance/storage.ts'
|
|
7
7
|
import type { ChatDriver, ChatEvent } from './drivers.ts'
|
|
8
8
|
import { resolveLaunch } from './launch.ts'
|
|
9
9
|
|
|
@@ -78,7 +78,7 @@ export function onPath(program: string): boolean {
|
|
|
78
78
|
/** Whether pi can reach DeepSeek: the key in the environment, or the one pi stored for itself when its owner logged in (`~/.pi/agent/auth.json`, a `deepseek` entry). */
|
|
79
79
|
export function piHasDeepseekKey(
|
|
80
80
|
env: NodeJS.ProcessEnv = process.env,
|
|
81
|
-
home: string =
|
|
81
|
+
home: string = personHome(env),
|
|
82
82
|
): boolean {
|
|
83
83
|
if (env.DEEPSEEK_API_KEY) return true
|
|
84
84
|
try {
|