acrylblends 1.4.0 → 1.5.1
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 +7 -4
- package/dist/index.js +191 -62
- 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/blank/a53d41e6ef42/src/app.ts +160 -0
- package/legacy/blank/a53d41e6ef42/src/routes.ts +79 -0
- package/legacy/layouts.json +30 -0
- package/lib/{client-modules-Bkdr83jO.js → client-modules-C5qiVroz.js} +1 -1
- package/lib/cordis/door.js +1 -1
- 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 +6 -2
- 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 +52 -7
- 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 +395 -0
- package/plugins/instance/index.ts +132 -24
- package/plugins/instance/storage.ts +413 -0
- package/shared/skills/acryl-remove-plugin/SKILL.md +1 -1
- package/templates/agent/README.md +1 -1
- package/templates/agent/gitignore +2 -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/README.md
CHANGED
|
@@ -397,10 +397,13 @@ plugins installed while the app runs, an agent that builds plugins, durable task
|
|
|
397
397
|
secrets):
|
|
398
398
|
|
|
399
399
|
```sh
|
|
400
|
-
npx acrylblends init my-app
|
|
401
|
-
cd my-app && npm install && npm run dev
|
|
400
|
+
npx acrylblends@latest init my-app -y
|
|
401
|
+
cd my-app && npm install && npm run dev # the app in the ACRYL frame: chat, agents, branches, versions, Settings
|
|
402
|
+
npm run desktop # the same in its own window
|
|
402
403
|
```
|
|
403
404
|
|
|
405
|
+
`--bare` makes the plain app without the frame.
|
|
406
|
+
|
|
404
407
|
See the [ACRYLBLENDS documentation](https://github.com/acrylblends/acrylblends/blob/main/docs/README.md) and
|
|
405
408
|
[Acryl in a Cordis app](https://github.com/acrylblends/acrylblends/blob/main/docs/framework/cordis/acryl-in-a-cordis-app.md).
|
|
406
409
|
|
package/dist/index.d.ts
CHANGED
|
@@ -53,7 +53,10 @@ interface FrameUrlRecord {
|
|
|
53
53
|
readonly pid: number;
|
|
54
54
|
readonly writtenAt: string;
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* The app home the frame uses, as the app's own family chooses it (`plugins/instance`, which its launcher asks too): the CLI never keeps a second copy
|
|
58
|
+
* of that rule. A folder without the family is not a frame app.
|
|
59
|
+
*/
|
|
57
60
|
export declare function frameHome(appRoot: string, env?: Record<string, string | undefined>): string;
|
|
58
61
|
export declare function frameUrlPath(home: string): string;
|
|
59
62
|
/** The token a frame address carries, if it carries one. */
|
|
@@ -116,13 +119,13 @@ interface InitOptions {
|
|
|
116
119
|
name: string | undefined;
|
|
117
120
|
template: Template | undefined;
|
|
118
121
|
install: boolean | undefined;
|
|
119
|
-
/**
|
|
122
|
+
/** In the ACRYL frame with the desktop shell (`add desktop`): the default; `--bare` makes a plain app. */
|
|
120
123
|
desktop: boolean;
|
|
121
124
|
/** Print no "Next:" block: `create-acrylblends` runs `init` and prints its own, for the package manager it knows. */
|
|
122
125
|
quietNext: boolean;
|
|
123
126
|
yes: boolean;
|
|
124
127
|
}
|
|
125
|
-
/** Parse `init [dir] [--template t] [--agent|--no-agent] [--name n] [--install] [-y]`. `--agent` means `--template agent`, `--no-agent` means `--template blank`. */
|
|
128
|
+
/** Parse `init [dir] [--template t] [--agent|--no-agent] [--name n] [--install] [--bare] [-y]`. `--agent` means `--template agent`, `--no-agent` means `--template blank`. */
|
|
126
129
|
export declare function parseInitOptions(argv: readonly string[]): InitOptions;
|
|
127
130
|
/** Scaffold a Cordis app. Returns the exit code: 0 done, 1 refused or failed. Never overwrites anything. */
|
|
128
131
|
export declare function init(options: InitOptions, io: Io): Promise<number>;
|
|
@@ -221,7 +224,7 @@ export declare function parseUpgradeOptions(argv: readonly string[]): UpgradeOpt
|
|
|
221
224
|
/**
|
|
222
225
|
* `acrylblends upgrade`: bring the library plugins an app carries (the router, the server, the door, `extensions`, ...) up to the version of this package. Each file is compared with what
|
|
223
226
|
* the app was given: an untouched file is replaced, a file the app edited stops the upgrade (it is shown as a diff and nothing changes) unless `--force`, which saves the edited version
|
|
224
|
-
* under `.
|
|
227
|
+
* under `.acryl/upgrade-backup/` first. Returns the exit code: 0 done or nothing to do, 1 refused or failed.
|
|
225
228
|
*/
|
|
226
229
|
export declare function upgrade(options: UpgradeOptions, io: Io): Promise<number>;
|
|
227
230
|
//#endregion
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,16 @@ import { homedir } from "node:os";
|
|
|
11
11
|
//#region ../../frame/packages.json
|
|
12
12
|
var scope = "@acrylblends";
|
|
13
13
|
var dshVersion = "0.2.0-rc.2";
|
|
14
|
+
var pnpm = {
|
|
15
|
+
"//": "An app in the frame installed with pnpm: these build scripts run, as they do under npm (packages/cli/src/pnpm-settings.ts). Found by installing a framed app with pnpm 12: dsh-subprocess-local builds its spawn helper, node-pty and koffi their native parts.",
|
|
16
|
+
"allowBuilds": [
|
|
17
|
+
"@deepseek-ai/dsh-subprocess-local",
|
|
18
|
+
"@google/genai",
|
|
19
|
+
"koffi",
|
|
20
|
+
"node-pty",
|
|
21
|
+
"protobufjs"
|
|
22
|
+
]
|
|
23
|
+
};
|
|
14
24
|
var packages = {
|
|
15
25
|
"acryl-ui": {
|
|
16
26
|
"name": "ui",
|
|
@@ -18,7 +28,7 @@ var packages = {
|
|
|
18
28
|
},
|
|
19
29
|
"dsh-client-ui-brand-acryl": {
|
|
20
30
|
"name": "brand-client",
|
|
21
|
-
"version": "0.1.
|
|
31
|
+
"version": "0.1.1"
|
|
22
32
|
},
|
|
23
33
|
"acryl-app-shell": {
|
|
24
34
|
"name": "app-shell",
|
|
@@ -70,14 +80,73 @@ var packages = {
|
|
|
70
80
|
},
|
|
71
81
|
"acryl-versions": {
|
|
72
82
|
"name": "versions",
|
|
73
|
-
"version": "0.1.
|
|
83
|
+
"version": "0.1.1"
|
|
74
84
|
},
|
|
75
85
|
"acryl-blends-frame": {
|
|
76
86
|
"name": "blends-frame",
|
|
77
|
-
"version": "0.1.
|
|
87
|
+
"version": "0.1.2"
|
|
78
88
|
}
|
|
79
89
|
};
|
|
80
90
|
//#endregion
|
|
91
|
+
//#region src/pnpm-settings.ts
|
|
92
|
+
/**
|
|
93
|
+
* What an app in the ACRYL frame needs from pnpm to install and start as it does with npm, kept in the app's `pnpm-workspace.yaml` (npm never reads it).
|
|
94
|
+
*
|
|
95
|
+
* - `nodeLinker: hoisted`: the frame's host composes about a hundred DSH rows by package name from the app's own folder, which finds them only in a flat
|
|
96
|
+
* `node_modules` (npm's layout). pnpm's isolated layout hides them, and the host stops with "failed to import" on every row.
|
|
97
|
+
* - `allowBuilds`: pnpm 11 and later refuse to finish an install while a dependency's build script is neither allowed nor denied. The frame's packages
|
|
98
|
+
* whose scripts run under npm (frame/packages.json `pnpm.allowBuilds`) are allowed, so pnpm installs what npm installs.
|
|
99
|
+
*
|
|
100
|
+
* The file is the person's too (pnpm itself writes `minimumReleaseAgeExclude` there), so it is edited by lines: a key they set is never changed, and
|
|
101
|
+
* their comments and layout stay.
|
|
102
|
+
*/
|
|
103
|
+
/** The frame's dependencies whose build scripts pnpm must run. */
|
|
104
|
+
const FRAME_ALLOW_BUILDS = pnpm.allowBuilds;
|
|
105
|
+
const PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
|
|
106
|
+
/** A top-level key line (`name:` at column 0), with or without a value after it. */
|
|
107
|
+
function topKey(line) {
|
|
108
|
+
return /^([A-Za-z][\w-]*):/u.exec(line)?.[1];
|
|
109
|
+
}
|
|
110
|
+
/** A YAML map key, quoted when it is not a plain word (a scoped package name). */
|
|
111
|
+
function yamlKey(name) {
|
|
112
|
+
return /^[\w.-]+$/u.test(name) ? name : `'${name}'`;
|
|
113
|
+
}
|
|
114
|
+
/** The package names a top-level `allowBuilds:` block already decides, allowed or denied. */
|
|
115
|
+
function decided(lines, start) {
|
|
116
|
+
const names = /* @__PURE__ */ new Set();
|
|
117
|
+
let end = start + 1;
|
|
118
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
119
|
+
const line = lines[index] ?? "";
|
|
120
|
+
if (line.trim() === "" || line.trimStart().startsWith("#")) continue;
|
|
121
|
+
if (!/^\s/u.test(line)) break;
|
|
122
|
+
const key = /^\s+(['"]?)(.+?)\1\s*:/u.exec(line)?.[2];
|
|
123
|
+
if (key !== void 0) names.add(key);
|
|
124
|
+
end = index + 1;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
names,
|
|
128
|
+
end
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* `pnpm-workspace.yaml` with the frame's settings added: the whole file when there is none, else only what it lacks. `builds` defaults to the frame's
|
|
133
|
+
* list. Returns the text unchanged when nothing is missing.
|
|
134
|
+
*/
|
|
135
|
+
function withFramePnpmSettings(text, builds = FRAME_ALLOW_BUILDS) {
|
|
136
|
+
const lines = text === void 0 || text === "" ? [] : text.replace(/\n$/u, "").split("\n");
|
|
137
|
+
if (!new Set(lines.map(topKey).filter((key) => key !== void 0)).has("nodeLinker")) lines.push("# The ACRYL frame finds its packages in a flat node_modules, as npm lays it out (acrylblends writes this; npm ignores the file).", "nodeLinker: hoisted");
|
|
138
|
+
const at = lines.findIndex((line) => topKey(line) === "allowBuilds");
|
|
139
|
+
if (at === -1) {
|
|
140
|
+
if (builds.length > 0) lines.push("# The frame's packages whose install scripts npm runs too; pnpm stops an install while a build is neither allowed nor denied.", "allowBuilds:", ...builds.map((name) => ` ${yamlKey(name)}: true`));
|
|
141
|
+
} else {
|
|
142
|
+
const { names, end } = decided(lines, at);
|
|
143
|
+
const missing = builds.filter((name) => !names.has(name));
|
|
144
|
+
if (missing.length > 0 && /^allowBuilds:\s*(#.*)?$/u.test(lines[at] ?? "")) lines.splice(end, 0, ...missing.map((name) => ` ${yamlKey(name)}: true`));
|
|
145
|
+
}
|
|
146
|
+
const next = lines.length === 0 ? "" : `${lines.join("\n")}\n`;
|
|
147
|
+
return text !== void 0 && next === text ? text : next;
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
81
150
|
//#region src/ide-profile.ts
|
|
82
151
|
/**
|
|
83
152
|
* The host an app runs on inside the ACRYL frame: the app's own node server and loader composition, with DSH riding as the chat/session driver
|
|
@@ -88,19 +157,17 @@ var packages = {
|
|
|
88
157
|
*/
|
|
89
158
|
/** Where an app keeps its data, for the plugins that inject `appInstance` (the framework's settings service does): the slice 0 row, kept in the app. */
|
|
90
159
|
const APP_INSTANCE_ROW = `// Generated by acrylblends: the app's own folder family, for the plugins that inject appInstance.
|
|
91
|
-
import {
|
|
160
|
+
import { fileURLToPath } from 'node:url'
|
|
161
|
+
import { selectInstance } from '../plugins/instance/index.ts'
|
|
92
162
|
|
|
93
163
|
export const name = 'app-instance'
|
|
94
164
|
|
|
95
165
|
export function apply(ctx) {
|
|
96
166
|
ctx.effect(() => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// The engine home ACRYL's plugins read (support serves \`dshHome/logs\`); in this host the engine and the app share one home.
|
|
102
|
-
dshHome: home,
|
|
103
|
-
})
|
|
167
|
+
// Chosen by the app's own family, as the host chose it: this folder, or the source a desktop product names. The engine home ACRYL's plugins read
|
|
168
|
+
// (support serves \`dshHome/logs\`) is the app's home.
|
|
169
|
+
const chosen = selectInstance({ projectRoot: fileURLToPath(new URL('..', import.meta.url)), env: process.env })
|
|
170
|
+
const instance = ctx.reflect.provide('appInstance', chosen)
|
|
104
171
|
return () => {
|
|
105
172
|
void instance()
|
|
106
173
|
}
|
|
@@ -525,9 +592,22 @@ function hostScriptOf(profile, frameModule) {
|
|
|
525
592
|
"import { DEFAULT_SCHEMA, dump as dumpYaml, Type, load as parseYaml } from 'js-yaml'",
|
|
526
593
|
"import { Acryl } from 'acrylblends'",
|
|
527
594
|
"",
|
|
528
|
-
"
|
|
595
|
+
"// The framework's files (this launcher, the composition it reads) sit next to it; the app is chosen by the app's own family: this folder, or the",
|
|
596
|
+
"// source a desktop product names when it runs the framework from its bundle. Its home and its port come with it, decided once.",
|
|
597
|
+
"const frameworkRoot = join(dirname(fileURLToPath(import.meta.url)), '..')",
|
|
598
|
+
"const { selectInstance } = await import(new URL('../plugins/instance/index.ts', import.meta.url).href)",
|
|
599
|
+
"const chosen = selectInstance({ projectRoot: frameworkRoot, env: process.env })",
|
|
600
|
+
"const appRoot = chosen.root",
|
|
601
|
+
"const home = chosen.home",
|
|
529
602
|
`const profileName = '${profile}'`,
|
|
530
|
-
"
|
|
603
|
+
"// The page and its tab are named for the app: the package's name, as the frame's header says it, else the folder's.",
|
|
604
|
+
"const appTitle = (() => {",
|
|
605
|
+
" try {",
|
|
606
|
+
" const name = JSON.parse(readFileSync(join(appRoot, 'package.json'), 'utf8')).name",
|
|
607
|
+
" if (typeof name === 'string' && name.trim() !== '') return name.trim()",
|
|
608
|
+
" } catch {}",
|
|
609
|
+
" return chosen.name",
|
|
610
|
+
"})()",
|
|
531
611
|
"// DSH's rows keep their files in this app's own home: set before any driver row is imported.",
|
|
532
612
|
"process.env.DSH_HOME = home",
|
|
533
613
|
"// Development only: the app's code can be loaded again in this host (a version gone back to, a file written by the chat, an agent or an editor). Every file",
|
|
@@ -543,8 +623,7 @@ function hostScriptOf(profile, frameModule) {
|
|
|
543
623
|
"const { boot } = await import('@deepseek-ai/dsh-app-boot')",
|
|
544
624
|
"// The frame listens where the app's own server would: the folder's stable port (or the home's, when ACRYLBLENDS_HOME pins one). A restart then",
|
|
545
625
|
"// keeps the page's origin, its auth cookie and its tabs; ACRYLBLENDS_PORT still decides, and the gates pass 0 for any free port.",
|
|
546
|
-
"const
|
|
547
|
-
"const port = String(selectInstance({ projectRoot: appRoot, env: process.env }).port.start)",
|
|
626
|
+
"const port = String(chosen.port.start)",
|
|
548
627
|
"const check = process.argv.includes('--check')",
|
|
549
628
|
"const require = createRequire(import.meta.url)",
|
|
550
629
|
"/** The `!!js` tag the loader evaluates: js-yaml needs the same type the Cordis include plugin declares. */",
|
|
@@ -593,7 +672,8 @@ function hostScriptOf(profile, frameModule) {
|
|
|
593
672
|
" return kept",
|
|
594
673
|
"}",
|
|
595
674
|
"",
|
|
596
|
-
"
|
|
675
|
+
"// The frame layer is the framework's file; its rows are anchored at the app, so the composition DSH keeps names the app's files, never the bundle's.",
|
|
676
|
+
"const frameLayer = anchorRows(load(readFileSync(join(frameworkRoot, 'cordis.patch.yml'), 'utf8')) ?? [], appRoot)",
|
|
597
677
|
"/** Our host rows first: the framework's node server, then the webServer port the driver rows register on (they are the app's, not DSH's). */",
|
|
598
678
|
`const clientRows = ${JSON.stringify(CLIENT_ROWS, null, 2).replaceAll("\\n", "\\n")}`,
|
|
599
679
|
`const agentRows = ${JSON.stringify(AGENT_PRESET_ROWS, null, 2)}`,
|
|
@@ -613,7 +693,7 @@ function hostScriptOf(profile, frameModule) {
|
|
|
613
693
|
" insert: [",
|
|
614
694
|
" { id: 'node-server', name: './plugins/acryl-node-server/src/index.ts', config: { hostname: '127.0.0.1', port: Number(port), scan: Number(port) === 0 ? 0 : 20 } },",
|
|
615
695
|
" { id: 'web-server', name: './plugins/acryl-web-server/src/index.ts' },",
|
|
616
|
-
" { id: 'page', name: './plugins/acryl-page/src/index.ts' },",
|
|
696
|
+
" { id: 'page', name: './plugins/acryl-page/src/index.ts', config: { title: appTitle } },",
|
|
617
697
|
" ...clientRows,",
|
|
618
698
|
" ...agentRows,",
|
|
619
699
|
" ],",
|
|
@@ -664,7 +744,7 @@ function hostScriptOf(profile, frameModule) {
|
|
|
664
744
|
"const layers = [compositionLayer, settingsLayer]",
|
|
665
745
|
"let ctx",
|
|
666
746
|
"try {",
|
|
667
|
-
" ctx = await boot('" + profile + "', join(
|
|
747
|
+
" ctx = await boot('" + profile + "', join(frameworkRoot, 'cordis.yml'), layers.flat(), (hostContext) => {",
|
|
668
748
|
" hostContext.provide('profileContext', profileContext)",
|
|
669
749
|
" }, pathToFileURL(join(appRoot, 'package.json')).href)",
|
|
670
750
|
"} catch (error) {",
|
|
@@ -673,10 +753,11 @@ function hostScriptOf(profile, frameModule) {
|
|
|
673
753
|
" const entries = Array.isArray(error?.entries) ? error.entries : []",
|
|
674
754
|
" const failures = entries.filter(({ outcome }) => outcome?.kind === \"failed\")",
|
|
675
755
|
" console.error(`acrylblends host: startup failed (${String(failures.length)} row(s))`)",
|
|
676
|
-
"
|
|
756
|
+
" // Each entry is the boot report of one row: { id, module, required, fiberState, outcome }.",
|
|
757
|
+
" for (const { id, module, outcome } of failures) {",
|
|
677
758
|
" const cause = outcome.error",
|
|
678
759
|
" const detail = cause instanceof Error ? cause.stack ?? cause.message : String(cause)",
|
|
679
|
-
" console.error(`- ${
|
|
760
|
+
" console.error(`- ${String(id)} (${String(module)}): ${detail}`)",
|
|
680
761
|
" }",
|
|
681
762
|
" const cause = error?.cause",
|
|
682
763
|
" if (failures.length === 0 && cause !== undefined) console.error(cause instanceof Error ? cause.stack ?? cause.message : String(cause))",
|
|
@@ -837,9 +918,10 @@ function overlayOf(packages, door = false, appRows = []) {
|
|
|
837
918
|
for (const entry of rows) {
|
|
838
919
|
lines.push(` - id: ${entry.row ?? entry.name}`, ` name: ${yamlName(entry.name)}`);
|
|
839
920
|
if ((entry.row ?? entry.name) === "acryl-blends-frame") lines.push(" config:", ` appPage: ${FRAME_PAGE_PREFIX}`);
|
|
921
|
+
if ((entry.row ?? entry.name) === "acryl-router") lines.push(" config:", " trailingSlash: ignore");
|
|
840
922
|
}
|
|
841
923
|
lines.push("- id: connection", " inject: []", " config:", " trustedHosts: ['127.0.0.1']");
|
|
842
|
-
lines.push("- id: workspace-controller", " config:", " documentsDirectory: !!js process.env.
|
|
924
|
+
lines.push("- id: workspace-controller", " config:", " documentsDirectory: !!js process.env.DSH_HOME");
|
|
843
925
|
for (const id of [...disabled].sort()) lines.push(`- id: ${id}`, " disabled: true");
|
|
844
926
|
return `${lines.join("\n")}\n`;
|
|
845
927
|
}
|
|
@@ -935,36 +1017,41 @@ function planIdeProfileWrite(root, input) {
|
|
|
935
1017
|
const hostPath = join(root, "scripts/ide-host.mjs");
|
|
936
1018
|
const appHost = existsSync(hostPath) ? readFileSync(hostPath, "utf8") : void 0;
|
|
937
1019
|
let clientRowsAdded = [];
|
|
1020
|
+
const files = GENERATED_FILES.map(({ file, of }) => {
|
|
1021
|
+
let text = of(plan);
|
|
1022
|
+
if (file === "scripts/ide-host.mjs") {
|
|
1023
|
+
const merged = mergeHostClientRows(text, appHost);
|
|
1024
|
+
text = merged.text;
|
|
1025
|
+
clientRowsAdded = merged.added;
|
|
1026
|
+
}
|
|
1027
|
+
const path = join(root, file);
|
|
1028
|
+
if (!existsSync(path)) return {
|
|
1029
|
+
file,
|
|
1030
|
+
text,
|
|
1031
|
+
state: "add"
|
|
1032
|
+
};
|
|
1033
|
+
const before = readFileSync(path, "utf8");
|
|
1034
|
+
if (before === text) return {
|
|
1035
|
+
file,
|
|
1036
|
+
text,
|
|
1037
|
+
state: "same"
|
|
1038
|
+
};
|
|
1039
|
+
const firstLine = before.split("\n", 1)[0] ?? "";
|
|
1040
|
+
return {
|
|
1041
|
+
file,
|
|
1042
|
+
text,
|
|
1043
|
+
state: GENERATED_MARKER.test(firstLine) ? "update" : "conflict"
|
|
1044
|
+
};
|
|
1045
|
+
});
|
|
1046
|
+
const workspaceFile = join(root, PNPM_WORKSPACE_FILE);
|
|
1047
|
+
const workspaceBefore = existsSync(workspaceFile) ? readFileSync(workspaceFile, "utf8") : void 0;
|
|
1048
|
+
const workspaceAfter = withFramePnpmSettings(workspaceBefore);
|
|
938
1049
|
return {
|
|
939
1050
|
plan,
|
|
940
|
-
files
|
|
941
|
-
let text = of(plan);
|
|
942
|
-
if (file === "scripts/ide-host.mjs") {
|
|
943
|
-
const merged = mergeHostClientRows(text, appHost);
|
|
944
|
-
text = merged.text;
|
|
945
|
-
clientRowsAdded = merged.added;
|
|
946
|
-
}
|
|
947
|
-
const path = join(root, file);
|
|
948
|
-
if (!existsSync(path)) return {
|
|
949
|
-
file,
|
|
950
|
-
text,
|
|
951
|
-
state: "add"
|
|
952
|
-
};
|
|
953
|
-
const before = readFileSync(path, "utf8");
|
|
954
|
-
if (before === text) return {
|
|
955
|
-
file,
|
|
956
|
-
text,
|
|
957
|
-
state: "same"
|
|
958
|
-
};
|
|
959
|
-
const firstLine = before.split("\n", 1)[0] ?? "";
|
|
960
|
-
return {
|
|
961
|
-
file,
|
|
962
|
-
text,
|
|
963
|
-
state: GENERATED_MARKER.test(firstLine) ? "update" : "conflict"
|
|
964
|
-
};
|
|
965
|
-
}),
|
|
1051
|
+
files,
|
|
966
1052
|
manifestChanged: existing !== plan.packageJson,
|
|
967
|
-
clientRowsAdded
|
|
1053
|
+
clientRowsAdded,
|
|
1054
|
+
pnpmWorkspace: workspaceAfter === workspaceBefore ? void 0 : workspaceAfter
|
|
968
1055
|
};
|
|
969
1056
|
}
|
|
970
1057
|
/** Write a plan made by {@link planIdeProfileWrite}: only the files whose bytes differ, and `package.json` only when the merge changes it. */
|
|
@@ -985,6 +1072,10 @@ function writeIdeProfilePlan(root, planned) {
|
|
|
985
1072
|
writeFileSync(join(root, "package.json"), planned.plan.packageJson);
|
|
986
1073
|
written.push("package.json");
|
|
987
1074
|
} else unchanged.push("package.json");
|
|
1075
|
+
if (planned.pnpmWorkspace !== void 0) {
|
|
1076
|
+
writeFileSync(join(root, PNPM_WORKSPACE_FILE), planned.pnpmWorkspace);
|
|
1077
|
+
written.push(PNPM_WORKSPACE_FILE);
|
|
1078
|
+
} else unchanged.push(PNPM_WORKSPACE_FILE);
|
|
988
1079
|
return {
|
|
989
1080
|
written,
|
|
990
1081
|
unchanged
|
|
@@ -1890,7 +1981,7 @@ function startRepository(directory) {
|
|
|
1890
1981
|
"Initial commit from acrylblends"
|
|
1891
1982
|
]);
|
|
1892
1983
|
}
|
|
1893
|
-
/** Parse `init [dir] [--template t] [--agent|--no-agent] [--name n] [--install] [-y]`. `--agent` means `--template agent`, `--no-agent` means `--template blank`. */
|
|
1984
|
+
/** Parse `init [dir] [--template t] [--agent|--no-agent] [--name n] [--install] [--bare] [-y]`. `--agent` means `--template agent`, `--no-agent` means `--template blank`. */
|
|
1894
1985
|
function parseInitOptions(argv) {
|
|
1895
1986
|
let parsed;
|
|
1896
1987
|
try {
|
|
@@ -1900,6 +1991,7 @@ function parseInitOptions(argv) {
|
|
|
1900
1991
|
}
|
|
1901
1992
|
const { values, positionals } = parsed;
|
|
1902
1993
|
if (positionals.length > 1) throw new UsageError("init takes at most one directory");
|
|
1994
|
+
if (values.bare === true && values.desktop === true) throw new UsageError("use --bare or --desktop, not both");
|
|
1903
1995
|
if (values.agent && values["no-agent"]) throw new UsageError("use --agent or --no-agent, not both");
|
|
1904
1996
|
const named = values.template === void 0 ? void 0 : resolveTemplate(values.template);
|
|
1905
1997
|
if (values.template !== void 0 && named === void 0) throw new UsageError(`unknown template "${values.template}" (${TEMPLATES.join(", ")})`);
|
|
@@ -1910,7 +2002,7 @@ function parseInitOptions(argv) {
|
|
|
1910
2002
|
name: values.name,
|
|
1911
2003
|
template: named ?? alias,
|
|
1912
2004
|
install: values.install,
|
|
1913
|
-
desktop: values.
|
|
2005
|
+
desktop: values.bare !== true,
|
|
1914
2006
|
quietNext: values["no-next"] === true,
|
|
1915
2007
|
yes: values.yes === true
|
|
1916
2008
|
};
|
|
@@ -1926,6 +2018,7 @@ const parse$3 = (argv) => parseArgs({
|
|
|
1926
2018
|
"no-agent": { type: "boolean" },
|
|
1927
2019
|
install: { type: "boolean" },
|
|
1928
2020
|
desktop: { type: "boolean" },
|
|
2021
|
+
bare: { type: "boolean" },
|
|
1929
2022
|
"no-next": { type: "boolean" },
|
|
1930
2023
|
yes: {
|
|
1931
2024
|
type: "boolean",
|
|
@@ -2141,7 +2234,8 @@ async function add(options, io) {
|
|
|
2141
2234
|
const code = addExternal(resolve(io.cwd, folder), appDir, io);
|
|
2142
2235
|
if (code !== 0) return code;
|
|
2143
2236
|
}
|
|
2144
|
-
const
|
|
2237
|
+
const folder = pluginsFolder(appDir).dir;
|
|
2238
|
+
const plain = !existsSync(join(appDir, "blend.yaml")) && (!existsSync(join(folder, "instance")) || existsSync(join(folder, "desktop-web")));
|
|
2145
2239
|
if (plain && options.names.includes("db")) {
|
|
2146
2240
|
io.stderr.write("error: \"db\" is the Cordis plugin for a Blend app. A plain Acryl app opens its database with openDb from \"acrylblends/db/sqlite\" (see docs/framework/guides/data.md)\n");
|
|
2147
2241
|
return 1;
|
|
@@ -2497,7 +2591,7 @@ function checkDeps(options, io) {
|
|
|
2497
2591
|
* The running frame's address, kept where the app's own data lives.
|
|
2498
2592
|
*
|
|
2499
2593
|
* The generated launcher prints the frame's URL with the launch token in it, and that token is the only way into a frame whose browser session ended.
|
|
2500
|
-
* So the address is written where the app keeps its data -
|
|
2594
|
+
* So the address is written where the app keeps its data - its folder under `~/.acryl`, outside the repository - at mint time,
|
|
2501
2595
|
* owner-only (0600 here; a restricted ACL on Windows), and atomically (a temp file in the same folder renamed over it, so a reader never sees half a
|
|
2502
2596
|
* record). On every later boot the launcher reuses the stored token instead of minting a new one, so the address - and an open page - survives a
|
|
2503
2597
|
* restart; `acrylblends open` prints it, and `acrylblends open --new` rotates it (the running host replaces the token in place through
|
|
@@ -2512,10 +2606,20 @@ function checkDeps(options, io) {
|
|
|
2512
2606
|
const FRAME_URL_FILE = "frame-url.json";
|
|
2513
2607
|
/** Where a running frame rotates its token: a route the launcher mounts on the frame's own router. */
|
|
2514
2608
|
const FRAME_TOKEN_PATH = "/api/acryl-frame/token/new";
|
|
2515
|
-
/**
|
|
2609
|
+
/** Where an app keeps its own family: the instance plugin its launcher imports. */
|
|
2610
|
+
const familyOf = (appRoot) => join(resolve(appRoot), "plugins", "instance", "index.ts");
|
|
2611
|
+
/**
|
|
2612
|
+
* The app home the frame uses, as the app's own family chooses it (`plugins/instance`, which its launcher asks too): the CLI never keeps a second copy
|
|
2613
|
+
* of that rule. A folder without the family is not a frame app.
|
|
2614
|
+
*/
|
|
2516
2615
|
function frameHome(appRoot, env = process.env) {
|
|
2517
|
-
const
|
|
2518
|
-
|
|
2616
|
+
const family = familyOf(appRoot);
|
|
2617
|
+
if (!existsSync(family)) throw new Error(`${appRoot} is not a frame app: it has no plugins/instance/index.ts`);
|
|
2618
|
+
const { selectInstance } = createRequire(import.meta.url)(family);
|
|
2619
|
+
return selectInstance({
|
|
2620
|
+
projectRoot: resolve(appRoot),
|
|
2621
|
+
env
|
|
2622
|
+
}).home;
|
|
2519
2623
|
}
|
|
2520
2624
|
function frameUrlPath(home) {
|
|
2521
2625
|
return join(home, FRAME_URL_FILE);
|
|
@@ -2674,6 +2778,10 @@ async function openFrame(args, io) {
|
|
|
2674
2778
|
return 2;
|
|
2675
2779
|
}
|
|
2676
2780
|
const directory = resolve(io.cwd, rest[0] ?? ".");
|
|
2781
|
+
if (!existsSync(familyOf(directory))) {
|
|
2782
|
+
io.stderr.write(`no frame has run in ${directory}: it is not a frame app (it has no plugins/instance)\n`);
|
|
2783
|
+
return 1;
|
|
2784
|
+
}
|
|
2677
2785
|
const found = await lookUpFrameUrl(directory);
|
|
2678
2786
|
if (found.kind === "none") {
|
|
2679
2787
|
io.stderr.write(`no frame has run in ${directory} yet; start it with npm run dev\n`);
|
|
@@ -3526,7 +3634,15 @@ function planFrameStep(appDir, library, templateDirFor = templateDir) {
|
|
|
3526
3634
|
file: broken
|
|
3527
3635
|
};
|
|
3528
3636
|
const template = identifyTemplate(appDir, name);
|
|
3529
|
-
if (template === void 0)
|
|
3637
|
+
if (template === void 0) {
|
|
3638
|
+
if (!ownsFrameRow(appDir)) return { kind: "unidentified" };
|
|
3639
|
+
const specs = frameProfileForApp(appDir);
|
|
3640
|
+
if (specs === void 0) return {
|
|
3641
|
+
kind: "unavailable",
|
|
3642
|
+
template: void 0
|
|
3643
|
+
};
|
|
3644
|
+
return planFrameWrites(appDir, library, name, specs, void 0, [], [], void 0);
|
|
3645
|
+
}
|
|
3530
3646
|
if (!carriesFrame(template, templateDirFor(template))) return {
|
|
3531
3647
|
kind: "no-frame-template",
|
|
3532
3648
|
template
|
|
@@ -3567,6 +3683,18 @@ function planFrameStep(appDir, library, templateDirFor = templateDir) {
|
|
|
3567
3683
|
else conflictLines = { "src/app.ts": ownerChangeLines(current, target) };
|
|
3568
3684
|
}
|
|
3569
3685
|
}
|
|
3686
|
+
return planFrameWrites(appDir, library, name, specs, template, files, kept, conflictLines);
|
|
3687
|
+
}
|
|
3688
|
+
/**
|
|
3689
|
+
* Whether an app built on its own carries what the frame mounts: its routes row (`src/frame.ts`), a `src/routes.ts` that says where the frame shows it, and
|
|
3690
|
+
* the composition the generated row calls (`composeInto` in `src/app.ts`). A registry blueprint made in the frame has all three.
|
|
3691
|
+
*/
|
|
3692
|
+
function ownsFrameRow(appDir) {
|
|
3693
|
+
const at = (file) => join(appDir, "src", file);
|
|
3694
|
+
return existsSync(at("frame.ts")) && existsSync(at("routes.ts")) && FRAME_PREFIX_EXPORT.test(readText(at("routes.ts"))) && existsSync(at("app.ts")) && carriesComposeInto(readText(at("app.ts")));
|
|
3695
|
+
}
|
|
3696
|
+
/** The part of the frame plan every framed app shares: the generated composition row, the host plugins, the launcher and overlay, and what conflicts. */
|
|
3697
|
+
function planFrameWrites(appDir, library, name, specs, template, files, kept, conflictLines) {
|
|
3570
3698
|
const appTs = files.find((file) => file.file === "src/app.ts")?.text ?? (existsSync(join(appDir, "src", "app.ts")) ? readText(join(appDir, "src", "app.ts")) : void 0);
|
|
3571
3699
|
const rows = appFrameRows(appDir, {
|
|
3572
3700
|
...appTs === void 0 ? {} : { appTs },
|
|
@@ -3621,7 +3749,7 @@ function frameReport(frame) {
|
|
|
3621
3749
|
/**
|
|
3622
3750
|
* `acrylblends upgrade`: bring the library plugins an app carries (the router, the server, the door, `extensions`, ...) up to the version of this package. Each file is compared with what
|
|
3623
3751
|
* the app was given: an untouched file is replaced, a file the app edited stops the upgrade (it is shown as a diff and nothing changes) unless `--force`, which saves the edited version
|
|
3624
|
-
* under `.
|
|
3752
|
+
* under `.acryl/upgrade-backup/` first. Returns the exit code: 0 done or nothing to do, 1 refused or failed.
|
|
3625
3753
|
*/
|
|
3626
3754
|
async function upgrade(options, io) {
|
|
3627
3755
|
const appDir = resolve(io.cwd, options.directory ?? ".");
|
|
@@ -3646,7 +3774,7 @@ async function upgrade(options, io) {
|
|
|
3646
3774
|
if (frame.kind === "planned" && frame.conflicts.length > 0 && !options.force) {
|
|
3647
3775
|
const own = frame.conflictLines?.["src/app.ts"] ?? [];
|
|
3648
3776
|
const detail = own.length === 0 ? "" : `\n src/app.ts keeps the framework's plugin registrations; the app's own lines are ${own.map((line) => `line ${String(line.line)} (${JSON.stringify(line.text)})`).join(", ")}`;
|
|
3649
|
-
io.stderr.write(`error: ${frame.conflicts.join(", ")} in ${appDir} ${frame.conflicts.length === 1 ? "is" : "are"} not the previous release's scaffold and acrylblends did not write ${frame.conflicts.length === 1 ? "it" : "them"}; nothing was changed.${detail} Move the app's version aside (or merge it into the template's) and run again, or run again with --force (the app's versions are saved under .
|
|
3777
|
+
io.stderr.write(`error: ${frame.conflicts.join(", ")} in ${appDir} ${frame.conflicts.length === 1 ? "is" : "are"} not the previous release's scaffold and acrylblends did not write ${frame.conflicts.length === 1 ? "it" : "them"}; nothing was changed.${detail} Move the app's version aside (or merge it into the template's) and run again, or run again with --force (the app's versions are saved under .acryl/upgrade-backup/ first)\n`);
|
|
3650
3778
|
return 1;
|
|
3651
3779
|
}
|
|
3652
3780
|
const frameChanged = frame.kind === "planned" && frame.changed;
|
|
@@ -3654,7 +3782,7 @@ async function upgrade(options, io) {
|
|
|
3654
3782
|
io.stdout.write(frame.kind === "planned" ? "nothing to upgrade: the library plugins and the frame are up to date\n" : `nothing to upgrade: the app carries no library plugins\n ${"frame".padEnd(22)} ${frameReport(frame)}\n`);
|
|
3655
3783
|
return 0;
|
|
3656
3784
|
}
|
|
3657
|
-
const scratch = join(appDir, ".
|
|
3785
|
+
const scratch = join(appDir, ".acryl", `upgrade-${process.pid}-${Date.now()}`);
|
|
3658
3786
|
const lock = readLock(folder.dir);
|
|
3659
3787
|
const history = readHistory();
|
|
3660
3788
|
const plans = [];
|
|
@@ -3732,7 +3860,7 @@ async function upgrade(options, io) {
|
|
|
3732
3860
|
io.stdout.write(`\n${state === "conflict" ? "EDITED HERE, the upgrade would replace it:\n" : ""}${unifiedDiff(name, before, after)}`);
|
|
3733
3861
|
}
|
|
3734
3862
|
if (conflicts > 0 && !options.force) {
|
|
3735
|
-
io.stderr.write(`\nerror: ${conflicts} file(s) were edited in this app and the upgrade would replace them; nothing was changed. Review the diffs above, move your changes out of the plugin (a plugin of your own in plugins/<name>/), or run again with --force (the edited versions are saved under .
|
|
3863
|
+
io.stderr.write(`\nerror: ${conflicts} file(s) were edited in this app and the upgrade would replace them; nothing was changed. Review the diffs above, move your changes out of the plugin (a plugin of your own in plugins/<name>/), or run again with --force (the edited versions are saved under .acryl/upgrade-backup/)\n`);
|
|
3736
3864
|
return 1;
|
|
3737
3865
|
}
|
|
3738
3866
|
if (options.dryRun) {
|
|
@@ -3743,7 +3871,7 @@ async function upgrade(options, io) {
|
|
|
3743
3871
|
io.stdout.write("\neverything is up to date\n");
|
|
3744
3872
|
return 0;
|
|
3745
3873
|
}
|
|
3746
|
-
const backup = join(appDir, ".
|
|
3874
|
+
const backup = join(appDir, ".acryl", "upgrade-backup", (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/gu, "-"));
|
|
3747
3875
|
const addedDependencies = [];
|
|
3748
3876
|
for (const plan of plans) {
|
|
3749
3877
|
const here = join(folder.dir, plan.plugin.name);
|
|
@@ -3843,7 +3971,8 @@ Options for init:
|
|
|
3843
3971
|
--template <name> one of the templates below (asked when not given); --agent means --template agent,
|
|
3844
3972
|
--no-agent means --template blank
|
|
3845
3973
|
--name <name> the project name (default: the directory name)
|
|
3846
|
-
--
|
|
3974
|
+
--bare a plain app, without the ACRYL frame (the frame, its chat and agents and npm run desktop, are the default)
|
|
3975
|
+
--desktop the frame and the desktop shell (the default; kept for older scripts)
|
|
3847
3976
|
--install run npm install after copying
|
|
3848
3977
|
-y, --yes ask nothing; use defaults (the blank template, no install)
|
|
3849
3978
|
|