acrylblends 1.1.0 → 1.3.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 +9 -0
- package/dist/index.d.ts +47 -2
- package/dist/index.js +2017 -32
- package/dist/index.js.map +1 -1
- package/docs/framework/agents/index.md +1 -1
- package/docs/framework/getting-started/desktop.md +23 -4
- package/docs/framework/guides/create-acrylblends.md +5 -3
- package/docs/framework/workshop/README.md +139 -153
- package/docs/framework/workshop/facilitator.md +69 -101
- package/legacy/README.md +9 -0
- package/legacy/agent/157ce3db1112/src/app.ts +98 -0
- package/legacy/agent/157ce3db1112/src/routes.ts +63 -0
- package/legacy/agent/24c681a00fca/src/app.ts +160 -0
- package/legacy/agent/24c681a00fca/src/routes.ts +83 -0
- package/legacy/agent/2e790da977b2/src/app.ts +98 -0
- package/legacy/agent/2e790da977b2/src/routes.ts +63 -0
- package/legacy/agent/6307045545dc/src/app.ts +168 -0
- package/legacy/agent/6307045545dc/src/routes.ts +83 -0
- package/legacy/agent/6a5fb9c15c53/src/app.ts +158 -0
- package/legacy/agent/6a5fb9c15c53/src/routes.ts +83 -0
- package/legacy/agent/71fb54a5a942/src/app.ts +111 -0
- package/legacy/agent/71fb54a5a942/src/routes.ts +63 -0
- package/legacy/agent/74ece148269c/src/app.ts +168 -0
- package/legacy/agent/74ece148269c/src/routes.ts +93 -0
- package/legacy/agent/855359304707/src/app.ts +168 -0
- package/legacy/agent/855359304707/src/routes.ts +126 -0
- package/legacy/agent/b5ef3c786ae4/src/app.ts +127 -0
- package/legacy/agent/b5ef3c786ae4/src/routes.ts +76 -0
- package/legacy/agent/c2f30958043b/src/app.ts +98 -0
- package/legacy/agent/c2f30958043b/src/routes.ts +53 -0
- package/legacy/blank/5a91c558af22/src/routes.ts +18 -0
- package/legacy/blank/9c650cf52bf3/src/routes.ts +14 -0
- package/legacy/blank/aea589283445/src/routes.ts +16 -0
- package/legacy/blank/b2cbb759c889/src/routes.ts +16 -0
- package/legacy/blank/d6d371c9a65e/src/routes.ts +18 -0
- package/legacy/chat/7076cc821e2c/src/app.ts +72 -0
- package/legacy/chat/7076cc821e2c/src/routes.ts +13 -0
- package/legacy/chat/7dbe6d5170df/src/app.ts +107 -0
- package/legacy/chat/7dbe6d5170df/src/routes.ts +19 -0
- package/legacy/chat/a9a9ab44c58e/src/app.ts +107 -0
- package/legacy/chat/a9a9ab44c58e/src/routes.ts +15 -0
- package/legacy/chat/f20ad9a51b06/src/app.ts +106 -0
- package/legacy/chat/f20ad9a51b06/src/routes.ts +15 -0
- package/legacy/layouts.json +524 -0
- package/legacy/nodejs/1d477f37aaf4/src/app.ts +92 -0
- package/legacy/nodejs/1d477f37aaf4/src/routes.ts +40 -0
- package/legacy/nodejs/b1bd9462496c/src/app.ts +61 -0
- package/legacy/nodejs/b1bd9462496c/src/routes.ts +37 -0
- package/legacy/nodejs/d5bab75076ef/src/app.ts +95 -0
- package/legacy/nodejs/d5bab75076ef/src/routes.ts +40 -0
- package/legacy/notes/4edc36949c15/src/app.ts +107 -0
- package/legacy/notes/4edc36949c15/src/routes.ts +14 -0
- package/legacy/notes/6cab2d09747c/src/app.ts +72 -0
- package/legacy/notes/6cab2d09747c/src/routes.ts +12 -0
- package/legacy/notes/7ca2dfed623a/src/app.ts +106 -0
- package/legacy/notes/7ca2dfed623a/src/routes.ts +14 -0
- package/legacy/todo/02e613b3625c/src/app.ts +71 -0
- package/legacy/todo/02e613b3625c/src/todo.ts +123 -0
- package/legacy/todo/45694a8d9e15/src/app.ts +71 -0
- package/legacy/todo/45694a8d9e15/src/todo.ts +96 -0
- package/legacy/todo/5923d3a6099f/src/app.ts +71 -0
- package/legacy/todo/5923d3a6099f/src/todo.ts +125 -0
- package/legacy/todo/849a18d66dc1/src/app.ts +91 -0
- package/legacy/todo/849a18d66dc1/src/todo.ts +125 -0
- package/legacy/todo/8e64211312f3/src/app.ts +71 -0
- package/legacy/todo/8e64211312f3/src/todo.ts +125 -0
- package/legacy/todo/b79078a87565/src/app.ts +71 -0
- package/legacy/todo/b79078a87565/src/todo.ts +125 -0
- package/lib/{audit-BFpULBCj.d.ts → audit-Bok_UNCn.d.ts} +26 -3
- package/lib/{client-modules-drj4mjhj.js → client-modules-Bkdr83jO.js} +1 -1
- package/lib/cordis/door.d.ts +87 -9
- package/lib/cordis/door.js +324 -60
- package/lib/cordis/inspect.js +1 -1
- package/lib/cordis/node-server.d.ts +101 -3
- package/lib/cordis/node-server.js +3 -3
- package/lib/cordis/router-http.d.ts +7 -1
- package/lib/cordis/router-http.js +44 -3
- package/lib/cordis/router.d.ts +1 -1
- package/lib/cordis/router.js +1 -1
- package/lib/cordis/state.js +1 -1
- package/lib/door/host.d.ts +2 -2
- package/lib/door/host.js +1 -1
- package/lib/door/page.d.ts +10 -1
- package/lib/door/page.js +46 -7
- package/lib/door/protocol.d.ts +1 -1
- package/lib/drivers.d.ts +10 -1
- package/lib/{index-DMOtA-NJ.d.ts → index-DGq7H0lb.d.ts} +11 -1
- package/lib/node-server.d.ts +7 -1
- package/lib/node-server.js +2 -2
- package/lib/{protocol-a2aqrp2X.d.ts → protocol-BeIYCgTo.d.ts} +4 -0
- package/lib/{snapshot-text-BwM6T2nG.js → snapshot-text-CXTnjxGx.js} +87 -7
- package/lib/src-CtkIX_Gg.js +427 -0
- package/lib/{src-BTBm4OPD.js → src-iBJ2ysV9.js} +77 -14
- package/lib/{test-websocket-BLeEvybj.js → test-websocket-CQOSvBth.js} +17 -7
- package/lib/{test-websocket-DyeS50Tr.d.ts → test-websocket-GUTAkFQT.d.ts} +3 -1
- package/package.json +2 -1
- package/plugins/acryl-door/plugin.json +3 -0
- package/plugins/acryl-door/src/cli/acryl.ts +63 -6
- package/plugins/acryl-door/src/host/channel.ts +86 -9
- package/plugins/acryl-door/src/host/chat-tools.ts +8 -6
- package/plugins/acryl-door/src/host/door-file.ts +7 -2
- package/plugins/acryl-door/src/host/door.ts +128 -24
- package/plugins/acryl-door/src/host/operator-routes.ts +21 -1
- package/plugins/acryl-door/src/host/page-routes.ts +3 -1
- package/plugins/acryl-door/src/host/page-socket.ts +18 -8
- package/plugins/acryl-door/src/host/verbs.ts +14 -1
- package/plugins/acryl-door/src/index.ts +168 -30
- package/plugins/acryl-door/src/page/base.ts +32 -0
- package/plugins/acryl-door/src/page/channel.ts +3 -1
- package/plugins/acryl-door/src/page/driver/accessibility.ts +11 -0
- package/plugins/acryl-door/src/page/driver/actions.ts +4 -0
- package/plugins/acryl-door/src/page/driver/driver.ts +32 -4
- package/plugins/acryl-door/src/page/driver/snapshot.ts +5 -2
- package/plugins/acryl-door/src/page/index.ts +41 -2
- package/plugins/acryl-door/src/page/outline.ts +56 -0
- package/plugins/acryl-door/src/page/protocol.ts +4 -0
- package/plugins/acryl-door/src/page/snapshot-text.ts +2 -1
- package/plugins/acryl-node-server/src/adapter/listener.ts +4 -0
- package/plugins/acryl-node-server/src/adapter/raw.ts +18 -0
- package/plugins/acryl-node-server/src/adapter/websocket.ts +15 -9
- package/plugins/acryl-node-server/src/client-modules.ts +7 -1
- package/plugins/acryl-node-server/src/index.ts +304 -16
- package/plugins/acryl-node-server/src/upgrade-hub.ts +2 -2
- package/plugins/acryl-page/plugin.json +12 -0
- package/plugins/acryl-page/src/index.ts +163 -0
- package/plugins/acryl-router/src/service.ts +77 -14
- package/plugins/acryl-router-http/src/index.ts +15 -1
- package/plugins/acryl-router-http/src/serve.ts +31 -0
- package/plugins/acryl-web-server/plugin.json +14 -0
- package/plugins/acryl-web-server/src/index.ts +164 -0
- package/plugins/agent/agent.ts +38 -7
- package/plugins/agent/index.ts +1 -1
- package/plugins/agent/model.ts +10 -2
- package/plugins/agent/tools.ts +9 -3
- package/plugins/agent-panel/client/describe.ts +57 -0
- package/plugins/agent-panel/client/driver-pick.ts +22 -0
- package/plugins/agent-panel/client/frame.ts +51 -0
- package/plugins/agent-panel/client/main.ts +238 -26
- package/plugins/agent-panel/drivers.ts +7 -2
- package/plugins/agent-panel/headless.ts +5 -2
- package/plugins/agent-panel/index.ts +146 -57
- package/plugins/agent-panel/mount.ts +27 -0
- package/plugins/agent-panel/visitors.ts +231 -0
- package/plugins/builder/index.ts +13 -35
- package/plugins/builder/package.ts +186 -0
- package/plugins/builder/plugin.json +3 -1
- package/plugins/desktop/frame.ts +231 -0
- package/plugins/desktop/launch.ts +44 -9
- package/plugins/desktop/main.ts +100 -32
- package/plugins/desktop/window.ts +21 -4
- package/plugins/driver-pty/client/terminal.ts +4 -1
- package/plugins/driver-pty/index.ts +38 -11
- package/plugins/driver-pty/spawn.ts +38 -0
- package/plugins/driver-pty/terminal.ts +2 -0
- package/plugins/extensions/index.ts +47 -18
- package/plugins/extensions/plugin-store.ts +27 -0
- package/plugins/extensions/plugin.json +1 -1
- package/plugins/extensions/resolve.ts +13 -0
- package/plugins/extensions/stage.ts +15 -5
- package/plugins/feedback/client.d.ts +22 -0
- package/plugins/feedback/client.js +471 -0
- package/plugins/feedback/index.ts +143 -0
- package/plugins/feedback/plugin.json +11 -0
- package/plugins/feedback/store.ts +307 -0
- package/plugins/home/index.ts +122 -0
- package/plugins/home/plugin.json +11 -0
- package/plugins/instance/index.ts +6 -0
- package/plugins/manifest/app-plugins.ts +86 -0
- package/plugins/manifest/index.ts +12 -2
- package/plugins/manifest/plugin.json +1 -1
- package/plugins/manifest/snapshot.ts +275 -0
- package/plugins/openai/index.ts +64 -5
- package/plugins/openai/prompt.ts +23 -0
- package/plugins/plugin-share/README.md +51 -0
- package/plugins/plugin-share/index.ts +467 -0
- package/plugins/plugin-share/plugin.json +15 -0
- package/plugins/plugin-share/publish.ts +65 -0
- package/plugins/plugin-share/pull-check.ts +129 -0
- package/plugins/plugin-share/pulled-records.ts +70 -0
- package/shared/AGENTS.core.md +36 -16
- package/shared/skills/acryl-add-ui/SKILL.md +19 -0
- package/shared/skills/acryl-app/SKILL.md +13 -0
- package/shared/skills/acryl-build-plugin/SKILL.md +22 -0
- package/shared/skills/acryl-change-plugin/SKILL.md +14 -0
- package/shared/skills/acryl-diagnose-plugin/SKILL.md +13 -0
- package/shared/skills/acryl-fix-plugin/SKILL.md +13 -0
- package/shared/skills/acryl-remove-plugin/SKILL.md +12 -0
- package/shared/skills/acryl-share-plugin/SKILL.md +16 -0
- package/shared/skills/cordis-plugin/SKILL.md +26 -0
- package/templates/agent/.gitattributes +2 -0
- package/templates/agent/AGENTS.md +1 -1
- package/templates/agent/blend.yaml +6 -0
- package/templates/agent/gitignore +1 -0
- package/templates/agent/package.json +1 -0
- package/templates/agent/src/app.ts +154 -33
- package/templates/agent/src/frame.ts +17 -0
- package/templates/agent/src/main.ts +5 -3
- package/templates/agent/src/routes.ts +38 -19
- package/templates/agent/template.json +3 -0
- package/templates/agent/test/app.test.ts +45 -3
- package/templates/blank/.gitattributes +2 -0
- package/templates/blank/AGENTS.md +3 -2
- package/templates/blank/blend.yaml +11 -0
- package/templates/blank/gitignore +1 -0
- package/templates/blank/package.json +1 -0
- package/templates/blank/src/app.ts +100 -29
- package/templates/blank/src/door.ts +7 -0
- package/templates/blank/src/first-page.ts +5 -0
- package/templates/blank/src/frame.ts +17 -0
- package/templates/blank/src/routes.ts +65 -8
- package/templates/blank/template.json +14 -1
- package/templates/blank/test/app.test.ts +103 -16
- package/templates/chat/.gitattributes +2 -0
- package/templates/chat/blend.yaml +2 -1
- package/templates/chat/gitignore +1 -0
- package/templates/chat/package.json +1 -0
- package/templates/chat/src/app.ts +72 -22
- package/templates/chat/src/chat-app.ts +11 -2
- package/templates/chat/src/client/chat.tsx +4 -2
- package/templates/chat/src/frame.ts +17 -0
- package/templates/chat/src/page.tsx +6 -4
- package/templates/chat/src/routes.ts +14 -4
- package/templates/chat/src/runtime.ts +9 -3
- package/templates/chat/template.json +3 -1
- package/templates/chat/test/chat.test.ts +46 -1
- package/templates/chat/test/server.test.ts +17 -3
- package/templates/nodejs/.gitattributes +2 -0
- package/templates/nodejs/blend.yaml +2 -1
- package/templates/nodejs/gitignore +1 -0
- package/templates/nodejs/package.json +1 -0
- package/templates/nodejs/src/app.ts +76 -21
- package/templates/nodejs/src/frame.ts +17 -0
- package/templates/nodejs/src/page.tsx +3 -3
- package/templates/nodejs/src/routes.ts +32 -13
- package/templates/nodejs/template.json +3 -1
- package/templates/nodejs/test/app.test.ts +38 -3
- package/templates/nodejs/test/server.test.ts +17 -3
- package/templates/notes/.gitattributes +2 -0
- package/templates/notes/blend.yaml +2 -1
- package/templates/notes/gitignore +1 -0
- package/templates/notes/package.json +1 -0
- package/templates/notes/src/app.ts +72 -22
- package/templates/notes/src/frame.ts +17 -0
- package/templates/notes/src/notes-app.ts +24 -17
- package/templates/notes/src/routes.ts +11 -4
- package/templates/notes/src/runtime.ts +9 -3
- package/templates/notes/src/views.tsx +26 -17
- package/templates/notes/template.json +3 -1
- package/templates/notes/test/notes.test.ts +39 -1
- package/templates/notes/test/server.test.ts +17 -3
- package/templates/todo/.gitattributes +2 -0
- package/templates/todo/blend.yaml +3 -0
- package/templates/todo/gitignore +1 -0
- package/templates/todo/package.json +1 -0
- package/templates/todo/src/app.ts +81 -27
- package/templates/todo/src/door.ts +7 -0
- package/templates/todo/src/frame.ts +17 -0
- package/templates/todo/src/page.ts +13 -6
- package/templates/todo/src/todo.ts +18 -6
- package/templates/todo/template.json +11 -1
- package/templates/todo/test/todo.test.ts +67 -9
- package/lib/src-LX0E4ec4.js +0 -117
- package/shared/SKILL.md +0 -14
package/plugins/builder/index.ts
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
* extensions folder, installs it, and gets back the real result (`active`, `pending` with what it waits for, or the error, with the
|
|
6
6
|
* previous version restored). Writes are confined to that folder. The tools register in `ctx.effect`, so they leave with the builder.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
9
9
|
import { dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path'
|
|
10
10
|
import type { Context } from '@deepseek-ai/cordis'
|
|
11
11
|
import { scripted, type Tools } from '../agent/index.ts'
|
|
12
12
|
import { NAME } from '../extensions/index.ts'
|
|
13
13
|
import type {} from '../instance/index.ts'
|
|
14
|
+
import { packagePlugin } from './package.ts'
|
|
14
15
|
import { builderRules } from './script.ts'
|
|
15
16
|
|
|
16
17
|
export interface BuilderConfig {
|
|
@@ -20,22 +21,6 @@ export interface BuilderConfig {
|
|
|
20
21
|
packagesDir?: string
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
/** The package.json of a standalone plugin package: the host framework is a peer, never a second installed copy. */
|
|
24
|
-
export function packageManifest(name: string, description: string): Record<string, unknown> {
|
|
25
|
-
return {
|
|
26
|
-
name,
|
|
27
|
-
version: '0.1.0',
|
|
28
|
-
description,
|
|
29
|
-
type: 'module',
|
|
30
|
-
exports: { '.': './index.ts', './package.json': './package.json' },
|
|
31
|
-
files: ['*.ts', '**/*.ts', 'README.md'],
|
|
32
|
-
// ACRYL measured it: a host package listed under dependencies brings a second copy of the framework, and its services stop
|
|
33
|
-
// matching after a restart. A peer names the version and installs nothing.
|
|
34
|
-
peerDependencies: { '@deepseek-ai/cordis': '4.0.4' },
|
|
35
|
-
keywords: ['cordis', 'cordis-plugin', 'acrylblends'],
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
24
|
const EXTENSIONS = new Set(['.ts', '.js', '.mjs', '.json', '.md'])
|
|
40
25
|
const MAX_BYTES = 256 * 1024
|
|
41
26
|
|
|
@@ -154,8 +139,8 @@ export const builder = {
|
|
|
154
139
|
)
|
|
155
140
|
register(
|
|
156
141
|
'package_plugin',
|
|
157
|
-
'Turn an extension into a standalone plugin package other apps can
|
|
158
|
-
(input) => {
|
|
142
|
+
'Turn an extension into a standalone plugin package other apps can pull: input { name, description? }. Writes the package folder and its release .tgz, the same bytes on every OS. Returns the folder, the file, its sha512 and its content digest.',
|
|
143
|
+
async (input) => {
|
|
159
144
|
const name = pluginName(input)
|
|
160
145
|
if (
|
|
161
146
|
!existsSync(join(folder(name), 'index.ts')) &&
|
|
@@ -166,24 +151,17 @@ export const builder = {
|
|
|
166
151
|
const description = String(
|
|
167
152
|
(input as { description?: unknown }).description ?? `The ${name} Cordis plugin.`,
|
|
168
153
|
)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
`${JSON.stringify(packageManifest(name, description), null, 2)}\n`,
|
|
179
|
-
)
|
|
180
|
-
if (!existsSync(join(target, 'README.md'))) {
|
|
181
|
-
writeFileSync(
|
|
182
|
-
join(target, 'README.md'),
|
|
183
|
-
`# ${name}\n\n${description}\n\nA Cordis plugin. In an ACRYLBLENDS app: \`acrylblends add <this folder>\`.\n`,
|
|
154
|
+
try {
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
name,
|
|
158
|
+
...(await packagePlugin(folder(name), packagesDir, name, description)),
|
|
159
|
+
}
|
|
160
|
+
} catch (cause) {
|
|
161
|
+
throw new BuilderError(
|
|
162
|
+
`${name}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
184
163
|
)
|
|
185
164
|
}
|
|
186
|
-
return { ok: true, name, dir: target }
|
|
187
165
|
},
|
|
188
166
|
)
|
|
189
167
|
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a plugin folder into what other people's apps can pull: a package folder and its release tarball, with the same bytes on every OS. The
|
|
3
|
+
* hub owns the tarball and the contract, blends-core what "portable" and "digest" mean; this module only decides what goes in.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
cpSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from 'node:fs'
|
|
14
|
+
import { extname, join } from 'node:path'
|
|
15
|
+
import { digestFiles, pluginFiles, portabilityIssues } from '@webboxes/blends-core'
|
|
16
|
+
import { packFolder, readTarball, syncServiceContract } from '@webboxes/plugin-hub'
|
|
17
|
+
|
|
18
|
+
/** The package.json of a standalone plugin package: the host framework is a peer, never a second installed copy. */
|
|
19
|
+
export function packageManifest(
|
|
20
|
+
name: string,
|
|
21
|
+
description: string,
|
|
22
|
+
version = '0.1.0',
|
|
23
|
+
release?: PackageRelease,
|
|
24
|
+
): Record<string, unknown> {
|
|
25
|
+
return {
|
|
26
|
+
name,
|
|
27
|
+
version,
|
|
28
|
+
description,
|
|
29
|
+
...(release === undefined
|
|
30
|
+
? {}
|
|
31
|
+
: {
|
|
32
|
+
license: release.license,
|
|
33
|
+
repository: {
|
|
34
|
+
type: 'git',
|
|
35
|
+
url: `git+https://github.com/${release.owner}/${release.repo}.git`,
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
type: 'module',
|
|
39
|
+
exports: { '.': './index.ts', './package.json': './package.json' },
|
|
40
|
+
// No `files` list: a plugin's plugin.json, data and .js files go with it, and the packer's own skips (node_modules, .git) apply.
|
|
41
|
+
// ACRYL measured it: a host package listed under dependencies brings a second copy of the framework, and its services stop
|
|
42
|
+
// matching after a restart. A peer names the version and installs nothing.
|
|
43
|
+
peerDependencies: { '@deepseek-ai/cordis': '4.0.4' },
|
|
44
|
+
keywords: ['cordis', 'cordis-plugin', 'acrylblends'],
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Where a package is going to be released: written in now, so what is planned is byte for byte what is released. */
|
|
49
|
+
export interface PackageRelease {
|
|
50
|
+
readonly owner: string
|
|
51
|
+
readonly repo: string
|
|
52
|
+
readonly license: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface PackageOptions {
|
|
56
|
+
/** This version, instead of the one already packaged. */
|
|
57
|
+
readonly version?: string
|
|
58
|
+
readonly release?: PackageRelease
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PackagedPlugin {
|
|
62
|
+
readonly dir: string
|
|
63
|
+
readonly version: string
|
|
64
|
+
readonly files: readonly string[]
|
|
65
|
+
/** The release file, next to the package folder. */
|
|
66
|
+
readonly tarball: string
|
|
67
|
+
/** The tarball's sha512, as the registry lists it. */
|
|
68
|
+
readonly integrity: string
|
|
69
|
+
/** The sha256 of the plugin's content, which a source can name (`@sha256:<digest>`). */
|
|
70
|
+
readonly digest: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Text whose line endings an editor or git on Windows may change: LF in the package, so those endings are not part of the plugin's identity. */
|
|
74
|
+
const TEXT = new Set([
|
|
75
|
+
'.ts',
|
|
76
|
+
'.mts',
|
|
77
|
+
'.cts',
|
|
78
|
+
'.tsx',
|
|
79
|
+
'.js',
|
|
80
|
+
'.mjs',
|
|
81
|
+
'.cjs',
|
|
82
|
+
'.jsx',
|
|
83
|
+
'.json',
|
|
84
|
+
'.md',
|
|
85
|
+
'.txt',
|
|
86
|
+
'.css',
|
|
87
|
+
'.html',
|
|
88
|
+
'.yml',
|
|
89
|
+
'.yaml',
|
|
90
|
+
'.svg',
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
function withLineFeeds(dir: string): void {
|
|
94
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
95
|
+
const path = join(dir, entry.name)
|
|
96
|
+
if (entry.isDirectory()) withLineFeeds(path)
|
|
97
|
+
else if (entry.isFile() && TEXT.has(extname(entry.name).toLowerCase())) {
|
|
98
|
+
const text = readFileSync(path, 'utf8')
|
|
99
|
+
if (text.includes('\r\n')) writeFileSync(path, text.replaceAll('\r\n', '\n'))
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** What the hub's share would otherwise add on the way out, changing the bytes after they were planned. */
|
|
105
|
+
function markPublic(target: string): void {
|
|
106
|
+
const file = join(target, 'package.json')
|
|
107
|
+
const manifest = JSON.parse(readFileSync(file, 'utf8')) as Record<string, unknown>
|
|
108
|
+
writeFileSync(
|
|
109
|
+
file,
|
|
110
|
+
`${JSON.stringify({ ...manifest, acryl: { ...(manifest.acryl as object), visibility: 'public' } }, null, 2)}\n`,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function versionIn(target: string): string | undefined {
|
|
115
|
+
try {
|
|
116
|
+
const { version } = JSON.parse(readFileSync(join(target, 'package.json'), 'utf8')) as {
|
|
117
|
+
version?: unknown
|
|
118
|
+
}
|
|
119
|
+
return typeof version === 'string' ? version : undefined
|
|
120
|
+
} catch {
|
|
121
|
+
return undefined
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Package the plugin in `source` as `<packages>/<name>/` and `<packages>/<name>-<version>.tgz`. The person's folder is not touched. A version already
|
|
127
|
+
* packaged stays: a release is made once, and a person bumps it deliberately.
|
|
128
|
+
* @throws when the plugin would not work on every OS or its contract cannot be read; no package is left.
|
|
129
|
+
*/
|
|
130
|
+
export async function packagePlugin(
|
|
131
|
+
source: string,
|
|
132
|
+
packages: string,
|
|
133
|
+
name: string,
|
|
134
|
+
description: string,
|
|
135
|
+
options: PackageOptions = {},
|
|
136
|
+
): Promise<PackagedPlugin> {
|
|
137
|
+
const target = join(packages, name)
|
|
138
|
+
const version = options.version ?? versionIn(target)
|
|
139
|
+
rmSync(target, { recursive: true, force: true })
|
|
140
|
+
mkdirSync(packages, { recursive: true })
|
|
141
|
+
cpSync(source, target, { recursive: true, filter: (path) => !path.includes('node_modules') })
|
|
142
|
+
try {
|
|
143
|
+
writeFileSync(
|
|
144
|
+
join(target, 'package.json'),
|
|
145
|
+
`${JSON.stringify(packageManifest(name, description, version, options.release), null, 2)}\n`,
|
|
146
|
+
)
|
|
147
|
+
if (!existsSync(join(target, 'README.md'))) {
|
|
148
|
+
writeFileSync(
|
|
149
|
+
join(target, 'README.md'),
|
|
150
|
+
`# ${name}\n\n${description}\n\nA Cordis plugin. Once it is released, pull it into an ACRYLBLENDS app while that app runs (\`npm run dev\`): \`npm run acryl -- plugin pull github:<owner>/<repo>\`.\n`,
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
withLineFeeds(target)
|
|
154
|
+
const issues = portabilityIssues(pluginFiles(target))
|
|
155
|
+
if (issues.length > 0)
|
|
156
|
+
throw new Error(
|
|
157
|
+
`it would not work on every OS: ${issues.map((issue) => `${issue.path}: ${issue.reason}`).join('; ')}`,
|
|
158
|
+
)
|
|
159
|
+
// The contract other people's apps and the registry read: what the plugin needs and provides, from its code.
|
|
160
|
+
const contract = await syncServiceContract(target)
|
|
161
|
+
if (!contract.ok)
|
|
162
|
+
throw new Error(`its service contract cannot be read: ${contract.errors.join('; ')}`)
|
|
163
|
+
if (options.release !== undefined) markPublic(target)
|
|
164
|
+
const packed = packFolder(target)
|
|
165
|
+
const tarball = join(packages, packed.fileName)
|
|
166
|
+
writeFileSync(tarball, packed.tarball)
|
|
167
|
+
const digest = digestFiles(
|
|
168
|
+
readTarball(packed.tarball).map(({ path, content }) => ({
|
|
169
|
+
path,
|
|
170
|
+
bytes: content,
|
|
171
|
+
kind: 'file' as const,
|
|
172
|
+
})),
|
|
173
|
+
)
|
|
174
|
+
return {
|
|
175
|
+
dir: target,
|
|
176
|
+
version: packed.version,
|
|
177
|
+
files: packed.files,
|
|
178
|
+
tarball,
|
|
179
|
+
integrity: packed.integrity,
|
|
180
|
+
digest,
|
|
181
|
+
}
|
|
182
|
+
} catch (cause) {
|
|
183
|
+
rmSync(target, { recursive: true, force: true })
|
|
184
|
+
throw cause
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
"inject": ["extensions", "tools", "appInstance"],
|
|
6
6
|
"provides": [],
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@deepseek-ai/cordis": "4.0.4"
|
|
8
|
+
"@deepseek-ai/cordis": "4.0.4",
|
|
9
|
+
"@webboxes/blends-core": "0.5.0",
|
|
10
|
+
"@webboxes/plugin-hub": "0.2.0"
|
|
9
11
|
},
|
|
10
12
|
"mount": "mount it after agent and extensions: root.plugin(builder, { dir: join(root, 'plugins') }); dir must be an extensions source"
|
|
11
13
|
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The desktop window on the ACRYL frame (docs/plans/ide-host-inversion.md, section 27). An app with `scripts/ide-host.mjs` runs inside the frame on `npm run dev`;
|
|
3
|
+
* `npm run desktop` shows that same frame: it attaches to a host that already runs (the host's address record, `.acrylblends/frame-url.json`), or starts the host
|
|
4
|
+
* the way `npm run dev` does and loads the address it prints. Nothing here imports electron: `main.ts` decides with these functions and owns the window.
|
|
5
|
+
*/
|
|
6
|
+
import { type ChildProcess, spawn, spawnSync } from 'node:child_process'
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
8
|
+
import { connect } from 'node:net'
|
|
9
|
+
import { join, resolve } from 'node:path'
|
|
10
|
+
|
|
11
|
+
/** The launcher every frame app has; without it the app runs on its own server (made before the frame). */
|
|
12
|
+
export const HOST_SCRIPT = join('scripts', 'ide-host.mjs')
|
|
13
|
+
|
|
14
|
+
/** The line the host prints once it serves the frame, with the launch token in the address. */
|
|
15
|
+
const HOST_LINE = /acrylblends host: (http:\/\/\S+)/u
|
|
16
|
+
|
|
17
|
+
const LOOPBACK = new Set(['127.0.0.1', 'localhost', '[::1]'])
|
|
18
|
+
|
|
19
|
+
/** How long a stopping host gets before its tree is killed. */
|
|
20
|
+
export const HOST_STOP_GRACE_MS = 5_000
|
|
21
|
+
|
|
22
|
+
export function isFrameApp(root: string, exists: (path: string) => boolean = existsSync): boolean {
|
|
23
|
+
return exists(join(root, HOST_SCRIPT))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The app home the host uses: `ACRYLBLENDS_HOME` when set, else `<app>/.acrylblends` (the launcher's own rule). */
|
|
27
|
+
export function frameHome(root: string, env: Record<string, string | undefined>): string {
|
|
28
|
+
const pinned = env.ACRYLBLENDS_HOME?.trim()
|
|
29
|
+
return pinned === undefined || pinned === ''
|
|
30
|
+
? join(resolve(root), '.acrylblends')
|
|
31
|
+
: resolve(pinned)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The frame address in the host's output so far, or `undefined` while it has not printed it. */
|
|
35
|
+
export function hostAddress(output: string): string | undefined {
|
|
36
|
+
return HOST_LINE.exec(output)?.[1]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A loopback `http:` address, or `undefined` for anything else: the window only ever opens a frame on this machine. */
|
|
40
|
+
export function loopbackAddress(url: unknown): URL | undefined {
|
|
41
|
+
if (typeof url !== 'string') return undefined
|
|
42
|
+
try {
|
|
43
|
+
const parsed = new URL(url)
|
|
44
|
+
return parsed.protocol === 'http:' && LOOPBACK.has(parsed.hostname) ? parsed : undefined
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RecordDeps {
|
|
51
|
+
readonly read?: (file: string) => string
|
|
52
|
+
/** Whether something accepts a connection at the address. */
|
|
53
|
+
readonly listening?: (url: URL) => Promise<boolean>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const tcpListening = (url: URL, timeoutMs = 500): Promise<boolean> =>
|
|
57
|
+
new Promise((done) => {
|
|
58
|
+
const socket = connect({
|
|
59
|
+
host: url.hostname.replace(/^\[|\]$/gu, ''),
|
|
60
|
+
port: Number(url.port || 80),
|
|
61
|
+
})
|
|
62
|
+
const finish = (listening: boolean) => {
|
|
63
|
+
socket.destroy()
|
|
64
|
+
done(listening)
|
|
65
|
+
}
|
|
66
|
+
socket.setTimeout(timeoutMs)
|
|
67
|
+
socket.once('connect', () => finish(true))
|
|
68
|
+
socket.once('timeout', () => finish(false))
|
|
69
|
+
socket.once('error', () => finish(false))
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The frame that already runs for this app: the host's address record, when it names a loopback address and something listens there. A record a stopped host
|
|
74
|
+
* left behind is not a running frame.
|
|
75
|
+
*/
|
|
76
|
+
export async function runningFrame(
|
|
77
|
+
home: string,
|
|
78
|
+
deps: RecordDeps = {},
|
|
79
|
+
): Promise<string | undefined> {
|
|
80
|
+
const read = deps.read ?? ((file: string) => readFileSync(file, 'utf8'))
|
|
81
|
+
let record: { url?: unknown }
|
|
82
|
+
try {
|
|
83
|
+
record = JSON.parse(read(join(home, 'frame-url.json'))) as { url?: unknown }
|
|
84
|
+
} catch {
|
|
85
|
+
return undefined
|
|
86
|
+
}
|
|
87
|
+
const url = loopbackAddress(record.url)
|
|
88
|
+
if (url === undefined) return undefined
|
|
89
|
+
return (await (deps.listening ?? tcpListening)(url)) ? url.href : undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** An address as the door may show it: the frame's launch token removed (the token is the way into the frame, so it never goes to a log). */
|
|
93
|
+
export function withoutToken(href: string): string {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = new URL(href)
|
|
96
|
+
if (!parsed.searchParams.has('token')) return href
|
|
97
|
+
parsed.searchParams.delete('token')
|
|
98
|
+
return parsed.href
|
|
99
|
+
} catch {
|
|
100
|
+
return href
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* What the Settings... menu item runs in a frame window: the window event the page's root owner opens its own Settings on (`acryl-blends-frame`'s
|
|
106
|
+
* `OPEN_SETTINGS_EVENT`, and the IDE's `acryl-workspace`; keep the string in step). The same dialog as the top bar's gear, never a second one.
|
|
107
|
+
*/
|
|
108
|
+
export const openFrameSettings = "window.dispatchEvent(new Event('acryl:open-settings'))"
|
|
109
|
+
|
|
110
|
+
/** What the window does with a navigation: stay on the frame's origin, hand the address to the system browser, or refuse it. */
|
|
111
|
+
export type Navigation = 'stay' | 'external' | 'block'
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* ACRYL Desktop's rule (`electron-shell-generation.ts`): only the main frame is checked, so the frame's tiles (the app's page in an iframe) navigate freely;
|
|
115
|
+
* the frame's own origin stays in the window, an `http(s)` or `mailto` address goes to the system browser, anything else is refused.
|
|
116
|
+
*/
|
|
117
|
+
export function navigation(target: string, origin: string, mainFrame = true): Navigation {
|
|
118
|
+
if (!mainFrame) return 'stay'
|
|
119
|
+
let parsed: URL
|
|
120
|
+
try {
|
|
121
|
+
parsed = new URL(target)
|
|
122
|
+
} catch {
|
|
123
|
+
return 'block'
|
|
124
|
+
}
|
|
125
|
+
if (parsed.origin === origin) return 'stay'
|
|
126
|
+
return parsed.protocol === 'http:' ||
|
|
127
|
+
parsed.protocol === 'https:' ||
|
|
128
|
+
parsed.protocol === 'mailto:'
|
|
129
|
+
? 'external'
|
|
130
|
+
: 'block'
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** How the host is started: the npm and Node that ran `npm run desktop`, never Electron's Node (node-pty and DSH's rows are built for Node's ABI). */
|
|
134
|
+
export function hostCommand(
|
|
135
|
+
env: Record<string, string | undefined>,
|
|
136
|
+
platform: NodeJS.Platform,
|
|
137
|
+
): { file: string; args: string[]; shell: boolean } {
|
|
138
|
+
const node = env.npm_node_execpath
|
|
139
|
+
const npm = env.npm_execpath
|
|
140
|
+
if (node !== undefined && node !== '' && npm !== undefined && npm !== '')
|
|
141
|
+
return /\.c?js$/u.test(npm)
|
|
142
|
+
? { file: node, args: [npm, 'run', 'dev'], shell: false }
|
|
143
|
+
: { file: npm, args: ['run', 'dev'], shell: platform === 'win32' }
|
|
144
|
+
// Started without npm (Electron run by hand): npm from the PATH, `npm.cmd` through the shell on Windows (scripts/lib/platform.mjs, npmSpawn).
|
|
145
|
+
return platform === 'win32'
|
|
146
|
+
? { file: 'npm.cmd', args: ['run', 'dev'], shell: true }
|
|
147
|
+
: { file: 'npm', args: ['run', 'dev'], shell: false }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface FrameHost {
|
|
151
|
+
/** The frame address the host printed, or a rejection with the host's output when it ended first. */
|
|
152
|
+
readonly ready: Promise<string>
|
|
153
|
+
/** Stop the host and what it started; resolves once it has exited. */
|
|
154
|
+
stop(): Promise<void>
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Start the app's frame host as `npm run dev` does, in the app folder; its output is passed through to `write`. */
|
|
158
|
+
export function startFrameHost(
|
|
159
|
+
root: string,
|
|
160
|
+
env: NodeJS.ProcessEnv,
|
|
161
|
+
write: (text: string) => void,
|
|
162
|
+
platform: NodeJS.Platform = process.platform,
|
|
163
|
+
): FrameHost {
|
|
164
|
+
const command = hostCommand(env, platform)
|
|
165
|
+
// The host leads its own process group on macOS and Linux, so a stop reaches npm, the host and every terminal it runs; Windows stops the tree with taskkill.
|
|
166
|
+
const child: ChildProcess = spawn(command.file, command.args, {
|
|
167
|
+
cwd: root,
|
|
168
|
+
env: { ...env, ELECTRON_RUN_AS_NODE: undefined },
|
|
169
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
170
|
+
shell: command.shell,
|
|
171
|
+
detached: platform !== 'win32',
|
|
172
|
+
windowsHide: true,
|
|
173
|
+
})
|
|
174
|
+
let output = ''
|
|
175
|
+
let exited = false
|
|
176
|
+
const exit = new Promise<void>((done) => {
|
|
177
|
+
child.once('exit', () => {
|
|
178
|
+
exited = true
|
|
179
|
+
done()
|
|
180
|
+
})
|
|
181
|
+
child.once('error', () => {
|
|
182
|
+
exited = true
|
|
183
|
+
done()
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
const ready = new Promise<string>((done, fail) => {
|
|
187
|
+
const take = (chunk: Buffer) => {
|
|
188
|
+
const text = chunk.toString()
|
|
189
|
+
write(text)
|
|
190
|
+
if (output.length < 200_000) output += text
|
|
191
|
+
const address = hostAddress(output)
|
|
192
|
+
if (address !== undefined) done(address)
|
|
193
|
+
}
|
|
194
|
+
child.stdout?.on('data', take)
|
|
195
|
+
child.stderr?.on('data', take)
|
|
196
|
+
void exit.then(() =>
|
|
197
|
+
fail(new Error(`the frame host ended before it served the frame:\n${output.slice(-3000)}`)),
|
|
198
|
+
)
|
|
199
|
+
})
|
|
200
|
+
const signal = (name: NodeJS.Signals) => {
|
|
201
|
+
if (child.pid === undefined) return
|
|
202
|
+
// Windows has no process groups and no SIGTERM a console process can answer: the whole tree (npm, the host, its terminals) is ended at once.
|
|
203
|
+
if (platform === 'win32') {
|
|
204
|
+
spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], {
|
|
205
|
+
stdio: 'ignore',
|
|
206
|
+
windowsHide: true,
|
|
207
|
+
})
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
process.kill(-child.pid, name)
|
|
212
|
+
} catch {
|
|
213
|
+
// The group is already gone.
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
ready,
|
|
218
|
+
async stop() {
|
|
219
|
+
if (exited) return
|
|
220
|
+
signal('SIGTERM')
|
|
221
|
+
const graceful = await Promise.race([
|
|
222
|
+
exit.then(() => true),
|
|
223
|
+
new Promise<boolean>((done) => setTimeout(() => done(false), HOST_STOP_GRACE_MS).unref()),
|
|
224
|
+
])
|
|
225
|
+
if (!graceful) {
|
|
226
|
+
signal('SIGKILL')
|
|
227
|
+
await exit
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* (Ubuntu 23.10 and later: `kernel.apparmor_restrict_unprivileged_userns=1`), Chromium's sandbox cannot start for an Electron that npm unpacked into node_modules, and Electron
|
|
4
4
|
* aborts before any of our code runs. There, and only there, it starts with `--no-sandbox` and says so; everywhere else Electron starts exactly as it does with the sandbox on.
|
|
5
5
|
*/
|
|
6
|
-
import { spawn } from 'node:child_process'
|
|
6
|
+
import { type ChildProcess, spawn } from 'node:child_process'
|
|
7
7
|
import { readFileSync } from 'node:fs'
|
|
8
8
|
import { createRequire } from 'node:module'
|
|
9
9
|
import { join } from 'node:path'
|
|
@@ -29,9 +29,45 @@ export const SANDBOX_NOTICE =
|
|
|
29
29
|
"desktop: this Linux restricts unprivileged user namespaces (kernel.apparmor_restrict_unprivileged_userns=1), so Electron's sandbox cannot start; starting with --no-sandbox. " +
|
|
30
30
|
'To keep the sandbox, give Electron an AppArmor profile that allows `userns` (the way the VS Code and Chrome packages do): see docs/framework/getting-started/desktop.md'
|
|
31
31
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
/**
|
|
33
|
+
* The arguments for Electron: the entry file, with `--no-sandbox` first when the sandbox is blocked. Chromium switches given to the script
|
|
34
|
+
* (`npm run desktop -- --remote-debugging-port=9222`) go before the entry; anything that is not a switch is dropped.
|
|
35
|
+
*/
|
|
36
|
+
export function electronArgs(
|
|
37
|
+
entry: string,
|
|
38
|
+
blocked: boolean,
|
|
39
|
+
extra: readonly string[] = [],
|
|
40
|
+
): string[] {
|
|
41
|
+
const switches = extra.filter((arg) => /^--[a-z][a-z0-9-]*(=.*)?$/u.test(arg))
|
|
42
|
+
return [...(blocked ? ['--no-sandbox'] : []), ...switches, entry]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The signals the launcher hands to Electron: Ctrl-C, a stop, and the terminal closing. */
|
|
46
|
+
export const FORWARDED_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'] as const
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Start Electron and make it end the way it was asked to. On macOS and Linux Electron leads its own process group, so a terminal's Ctrl-C (which goes to the
|
|
50
|
+
* whole foreground group) reaches only this launcher, which hands Electron exactly one signal. Electron answers a first SIGINT, SIGTERM or SIGHUP with a
|
|
51
|
+
* graceful quit, the one that stops the frame host the window started; a second signal kills it at once, before any of that runs, and a Ctrl-C that reached
|
|
52
|
+
* both processes was two, as is one that npm passes on after the terminal sent it. Later signals are not passed on. Windows has no process groups and no such signals; there Electron starts as before. This process ends with Electron's exit code.
|
|
53
|
+
*/
|
|
54
|
+
export function runElectron(
|
|
55
|
+
file: string,
|
|
56
|
+
args: readonly string[],
|
|
57
|
+
platform: NodeJS.Platform = process.platform,
|
|
58
|
+
): ChildProcess {
|
|
59
|
+
const child = spawn(file, args, { stdio: 'inherit', detached: platform !== 'win32' })
|
|
60
|
+
// One signal, ever: npm hands a Ctrl-C to this process a second time, and Electron's quit is bounded (DESKTOP_SHUTDOWN_TIMEOUT_MS) without another.
|
|
61
|
+
let forwarded = false
|
|
62
|
+
for (const signal of FORWARDED_SIGNALS)
|
|
63
|
+
if (platform !== 'win32' || signal !== 'SIGHUP')
|
|
64
|
+
process.on(signal, () => {
|
|
65
|
+
if (forwarded) return
|
|
66
|
+
forwarded = true
|
|
67
|
+
child.kill(signal)
|
|
68
|
+
})
|
|
69
|
+
child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)))
|
|
70
|
+
return child
|
|
35
71
|
}
|
|
36
72
|
|
|
37
73
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
@@ -40,9 +76,8 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
|
40
76
|
const blocked =
|
|
41
77
|
process.env.ACRYLBLENDS_DESKTOP_SANDBOX !== '1' && sandboxBlocked(process.platform)
|
|
42
78
|
if (blocked) console.warn(SANDBOX_NOTICE)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)))
|
|
79
|
+
runElectron(
|
|
80
|
+
electron,
|
|
81
|
+
electronArgs(join(import.meta.dirname, 'main.ts'), blocked, process.argv.slice(2)),
|
|
82
|
+
)
|
|
48
83
|
}
|