@jxsuite/studio 3.0.0 → 4.1.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 -5
- package/dist/chunks/{2020-xp8vnwma.js → 2020-n6tfwnda.js} +2 -2
- package/dist/chunks/Overlay.dev-etqjwyf9.js +23 -0
- package/dist/chunks/cf-account-picker-en86v7x7.js +163 -0
- package/dist/chunks/{cf-settings-zj2cj1ar.js → cf-settings-45fgyx8y.js} +6 -5
- package/dist/chunks/{components-z6cnkyy1.js → components-zmx10t62.js} +6 -5
- package/dist/chunks/content-source-48xg86wj.js +49 -0
- package/dist/chunks/{convert-to-repeater-6vvx96n2.js → convert-to-repeater-43tfn7cs.js} +33 -25
- package/dist/chunks/entry-commands-e63f1xc0.js +46 -0
- package/dist/chunks/{extension-sections-kwzvpemr.js → extension-sections-txv08xxg.js} +34 -26
- package/dist/chunks/file-ops-wbh4hhsx.js +48 -0
- package/dist/chunks/files-3kfkwhk4.js +93 -0
- package/dist/chunks/{focus-trap.esm-ggawf1hy.js → focus-trap.esm-9t2drp0g.js} +6 -5
- package/dist/chunks/{format-host-8fr3545e.js → format-host-w16q81tf.js} +3 -3
- package/dist/chunks/fs-events-hrrg328g.js +21 -0
- package/dist/chunks/{git-panel-g01d93vc.js → git-panel-532whr1y.js} +22 -16
- package/dist/chunks/{index-nj44jpzj.js → index-wcnt3r9g.js} +2 -2
- package/dist/chunks/{jx-validate-jj9rnt4n.js → jx-validate-8er6gbxy.js} +6 -4
- package/dist/chunks/layers-panel-07hm7g5j.js +67 -0
- package/dist/chunks/layers-q75tj2mf.js +45 -0
- package/dist/chunks/lit-html-j055mkh8.js +22 -0
- package/dist/chunks/monaco-buffer-dqw0xxpx.js +25 -0
- package/dist/chunks/{monaco-setup-a5wkmbgh.js → monaco-setup-vx9bhp1d.js} +4 -4
- package/dist/chunks/notify-yjm79n4s.js +30 -0
- package/dist/chunks/parse-24d0gay3.js +19 -0
- package/dist/chunks/platform-ba1j1gbh.js +16 -0
- package/dist/chunks/publish-panel-v9st96ja.js +29 -0
- package/dist/chunks/quick-search-zccjp6gv.js +66 -0
- package/dist/chunks/references-3qx8fkyp.js +29 -0
- package/dist/chunks/right-panel-vm3b6kwy.js +46 -0
- package/dist/chunks/sp-overlay.dev-qgvhnxf4.js +18 -0
- package/dist/chunks/sp-popover.dev-n2etmqdp.js +9 -0
- package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +15 -0
- package/dist/chunks/sp-tooltip.dev-4sastdnw.js +15 -0
- package/dist/chunks/{sp-tray.dev-64v4pxw8.js → sp-tray.dev-tkbscm06.js} +5 -5
- package/dist/chunks/{studio-wdt9y92a.js → studio-0bq6eh7v.js} +2 -2
- package/dist/chunks/studio-1ngbejft.js +181 -0
- package/dist/chunks/studio-1wagb550.js +118 -0
- package/dist/chunks/{studio-j595e748.js → studio-2b946z26.js} +172 -95
- package/dist/chunks/{studio-pkp61mh8.js → studio-3bqpr0zh.js} +25 -17
- package/dist/chunks/{studio-k4jvjcfx.js → studio-4b5z1x43.js} +46 -12
- package/dist/chunks/{studio-ry1vz5sv.js → studio-4n2b5gzb.js} +15 -13
- package/dist/chunks/{studio-p5dja96s.js → studio-4r1pv6zf.js} +2 -2
- package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
- package/dist/chunks/{studio-tzx3xmvx.js → studio-5fx3g9jg.js} +33 -52
- package/dist/chunks/{studio-rh1cnamv.js → studio-5myknm0a.js} +565 -135
- package/dist/chunks/studio-6yw4y3my.js +91 -0
- package/dist/chunks/{studio-0rqqhrdy.js → studio-7nte2hj4.js} +2 -2
- package/dist/chunks/studio-92ab2jqe.js +282 -0
- package/dist/chunks/{studio-6z5eyjwn.js → studio-950k0dx6.js} +72 -135
- package/dist/chunks/{studio-sas7mt3k.js → studio-9ab8gqsm.js} +2 -2
- package/dist/chunks/studio-9n4mrmnb.js +184 -0
- package/dist/chunks/{studio-hcxzyk3x.js → studio-ahn3pdgx.js} +6 -6
- package/dist/chunks/studio-b55y6ewx.js +498 -0
- package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
- package/dist/chunks/studio-cft9r4r7.js +119 -0
- package/dist/chunks/{studio-dc8s8heb.js → studio-cmb03bg5.js} +6904 -8321
- package/dist/chunks/studio-dm19q43r.js +195 -0
- package/dist/chunks/{studio-2x9zbxmx.js → studio-ef49pnzx.js} +11 -9
- package/dist/chunks/{studio-ay8wcr2b.js → studio-eqq1w638.js} +7 -9
- package/dist/chunks/{studio-5efrtn1m.js → studio-ew0djmhg.js} +23 -19
- package/dist/chunks/{studio-tr20m8g8.js → studio-f3wqjtk5.js} +39 -3
- package/dist/chunks/{studio-xntfdy1x.js → studio-fbf92ckf.js} +36 -23
- package/dist/chunks/{studio-4q0wznzf.js → studio-gcye7qka.js} +442 -126
- package/dist/chunks/{studio-7p35254k.js → studio-gfgb8pe6.js} +36 -193
- package/dist/chunks/studio-jc994dnr.js +1066 -0
- package/dist/chunks/{studio-hj2se583.js → studio-jkftxyvz.js} +69 -3423
- package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
- package/dist/chunks/{studio-kfpek536.js → studio-kyh9avvv.js} +2 -2
- package/dist/chunks/{studio-twsafr1e.js → studio-m3hwn7y1.js} +3 -3
- package/dist/chunks/studio-msd847zn.js +1315 -0
- package/dist/chunks/{studio-g5gdqnyd.js → studio-mtfnaqs9.js} +19 -19
- package/dist/chunks/{studio-7ejemhej.js → studio-n6j9t10q.js} +6 -6
- package/dist/chunks/{studio-vhk5fmfv.js → studio-n8b9wqwy.js} +5 -5
- package/dist/chunks/{studio-cjm7fdjf.js → studio-p4ngc046.js} +8 -104
- package/dist/chunks/studio-q43a5y9h.js +394 -0
- package/dist/chunks/studio-q7v10cg2.js +106 -0
- package/dist/chunks/{studio-6kv3apa9.js → studio-q9jd1a60.js} +27 -11
- package/dist/chunks/studio-qcy05dp8.js +1743 -0
- package/dist/chunks/{studio-a0yp7syn.js → studio-qjy2z9bv.js} +29 -132
- package/dist/chunks/{studio-2w11grav.js → studio-qq6jym04.js} +3 -3
- package/dist/chunks/{studio-3fk1ac9z.js → studio-rdx8jcxy.js} +4 -4
- package/dist/chunks/{studio-4mqb9a0j.js → studio-rn9ta2mz.js} +4 -4
- package/dist/chunks/studio-t7qb9db8.js +665 -0
- package/dist/chunks/studio-tmc5emrw.js +8 -0
- package/dist/chunks/{studio-r5xbtctf.js → studio-trxrabqf.js} +292 -2297
- package/dist/chunks/studio-wcy8trfx.js +263 -0
- package/dist/chunks/{studio-3dkcd4c5.js → studio-x4qkdat5.js} +62 -57
- package/dist/chunks/studio-x813zp48.js +195 -0
- package/dist/chunks/{studio-qpfq7xhm.js → studio-xrapxwwc.js} +22 -285
- package/dist/chunks/{studio-xm3dbxvn.js → studio-zczps70q.js} +2 -2
- package/dist/chunks/{studio-8ezdegcx.js → studio-zqazsct2.js} +40 -36
- package/dist/chunks/workspace-fjx52ryv.js +89 -0
- package/dist/iframe-entry.js +141 -56
- package/dist/manifest.json +7 -0
- package/dist/studio.js +2605 -1950
- package/favicon.ico +0 -0
- package/index.html +1 -0
- package/package.json +20 -18
- package/src/about/about-modal.ts +4 -3
- package/src/browse/library-pane.ts +57 -3
- package/src/canvas/canvas-render.ts +23 -1
- package/src/canvas/iframe-patch.ts +47 -21
- package/src/canvas/iframe-render.ts +22 -6
- package/src/commands/app-commands.ts +4 -0
- package/src/commands/command-args.ts +29 -0
- package/src/commands/context.ts +15 -1
- package/src/commands/defaults.ts +29 -1
- package/src/commands/levels.ts +16 -0
- package/src/commands/live-context.ts +3 -0
- package/src/content/collection-match.ts +267 -0
- package/src/content/entry-commands.ts +38 -29
- package/src/content/entry-fields.ts +6 -0
- package/src/editor/shortcuts.ts +5 -0
- package/src/files/file-ops.ts +10 -42
- package/src/files/files.ts +487 -59
- package/src/files/fs-events.ts +9 -0
- package/src/files/gitignore.ts +432 -0
- package/src/files/media-paths.ts +13 -42
- package/src/files/media-usage.ts +41 -131
- package/src/files/serialize-document.ts +60 -0
- package/src/format/convert-file.ts +511 -0
- package/src/format/format-choices.ts +199 -0
- package/src/grid/grid-view.ts +16 -2
- package/src/hosting/document.ts +2 -0
- package/src/hosting/layout.ts +8 -0
- package/src/media/media-open.ts +58 -0
- package/src/media/media-pane.ts +310 -0
- package/src/new-project/import-tab.ts +357 -163
- package/src/new-project/location-fields.ts +8 -5
- package/src/new-project/new-project-modal.ts +27 -34
- package/src/packages/ensure-deps.ts +14 -7
- package/src/packages/jxsuite-update.ts +11 -11
- package/src/page-params.ts +10 -46
- package/src/panels/activity-bar.ts +45 -16
- package/src/panels/ai-chat/attached-context.ts +6 -1
- package/src/panels/ai-chat/chat-view.ts +255 -13
- package/src/panels/ai-chat/composer.ts +31 -97
- package/src/panels/ai-panel.ts +116 -2
- package/src/panels/navigator-panels.ts +7 -6
- package/src/panels/settings-menu.ts +799 -0
- package/src/panels/toolbar.ts +135 -15
- package/src/platforms/cloud.ts +261 -70
- package/src/platforms/devserver.ts +67 -33
- package/src/preview/preview-overlay.ts +226 -0
- package/src/publish/publish-panel.ts +62 -2
- package/src/services/ai-ask.ts +247 -0
- package/src/services/ai-import-tools.ts +532 -0
- package/src/services/ai-models.ts +108 -1
- package/src/services/ai-project-tools.ts +20 -26
- package/src/services/ai-system-prompt.ts +88 -2
- package/src/services/ai-turn-signal.ts +70 -0
- package/src/services/connection.ts +74 -0
- package/src/services/context-manager.ts +101 -1
- package/src/services/document-assistant.ts +45 -4
- package/src/services/import-client.ts +33 -6
- package/src/services/import-run.ts +158 -0
- package/src/services/import-seed.ts +120 -0
- package/src/services/jx-validate.ts +58 -6
- package/src/services/project-adoption.ts +71 -0
- package/src/services/references.ts +0 -0
- package/src/services/settings/definitions.ts +17 -0
- package/src/services/tool-executor.ts +45 -2
- package/src/settings/contexts-section.ts +45 -3
- package/src/settings/dependencies-editor.ts +43 -23
- package/src/settings/general-settings.ts +30 -12
- package/src/settings/preferences-accounts.ts +193 -12
- package/src/settings/preferences-dialog.ts +93 -59
- package/src/settings/preferences-sections.ts +57 -0
- package/src/settings/settings-document.ts +65 -9
- package/src/studio.ts +33 -6
- package/src/tabs/transact.ts +42 -19
- package/src/types.ts +129 -7
- package/src/ui/ai-managed-connect.ts +62 -13
- package/src/ui/ai-model-picker.ts +189 -0
- package/src/ui/cf-account-picker.ts +200 -0
- package/src/ui/choice-field.ts +72 -0
- package/src/ui/layers.ts +83 -5
- package/src/utils/studio-utils.ts +22 -52
- package/styles/forced-colors.css +7 -0
- package/styles/overlays.css +256 -0
- package/styles/panels.css +13 -1
- package/styles/shell.css +147 -0
- package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
- package/dist/chunks/content-source-7fy9qrm0.js +0 -40
- package/dist/chunks/files-xcybanxe.js +0 -70
- package/dist/chunks/layers-panel-27essgkc.js +0 -60
- package/dist/chunks/publish-panel-09sy2bab.js +0 -22
- package/dist/chunks/quick-search-sefb61v8.js +0 -55
- package/dist/chunks/right-panel-9r32jd5j.js +0 -39
- package/dist/chunks/sp-overlay.dev-s18hymn9.js +0 -18
- package/dist/chunks/sp-popover.dev-dj9x7e7a.js +0 -9
- package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +0 -15
- package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +0 -15
- package/dist/chunks/studio-bw5dz6km.js +0 -608
- package/dist/chunks/studio-c31yb4t6.js +0 -55
- package/src/canvas/site-style-css.ts +0 -117
|
@@ -1,22 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The New Project modal's Import source step: an AI-guided clone of an existing site. Gated behind
|
|
3
3
|
* the AI credentials gate (key form + the keyless Cloudflare option) until AI is usable — a local
|
|
4
|
-
* key, or a backend holding its own; otherwise a URL
|
|
5
|
-
* parameters live on the modal's shared Parameters step
|
|
6
|
-
*
|
|
4
|
+
* key, or a backend holding its own; otherwise a URL, crawl options, a model and a brief. The
|
|
5
|
+
* name/directory parameters live on the modal's shared Parameters step.
|
|
6
|
+
*
|
|
7
|
+
* **This form no longer runs the import.** It gathers a brief and hands it to the assistant, which
|
|
8
|
+
* runs the pipeline as a tool call. The wizard used to own the run, and the consequence was that a
|
|
9
|
+
* successful import destroyed its own account of itself: every phase line and every warning lived
|
|
10
|
+
* in a log that vanished with the modal at the moment it handed off. In the transcript the run
|
|
11
|
+
* survives, the model can read what the crawl actually found, and it can stop and ask about
|
|
12
|
+
* anything the pipeline had to guess at.
|
|
7
13
|
*/
|
|
8
14
|
|
|
9
|
-
import { html } from "lit-html";
|
|
15
|
+
import { html, nothing } from "lit-html";
|
|
16
|
+
import { live } from "lit/directives/live.js";
|
|
10
17
|
import { getPlatform } from "../platform";
|
|
11
|
-
import { getBaseUrl, getOpenAiKey } from "../services/ai-settings";
|
|
12
18
|
import { preferredModel } from "../services/ai-models";
|
|
13
|
-
import {
|
|
14
|
-
import { notify } from "../services/notify";
|
|
15
|
-
import { errorMessage } from "@jxsuite/schema/parse";
|
|
19
|
+
import { createModelPicker } from "../ui/ai-model-picker";
|
|
16
20
|
import { destinationPath } from "./location-fields";
|
|
17
21
|
import type { TemplateResult } from "lit-html";
|
|
18
|
-
import type {
|
|
19
|
-
|
|
22
|
+
import type { CreateProjectDestination, ImportBreakpointPolicy } from "../types";
|
|
23
|
+
|
|
24
|
+
import { setPendingImportBrief } from "../services/import-seed";
|
|
25
|
+
import type { ImportBrief } from "../services/import-seed";
|
|
26
|
+
|
|
27
|
+
/** The three answers the Breakpoints control offers. */
|
|
28
|
+
type BreakpointMode = "all" | "limit" | "explicit";
|
|
29
|
+
|
|
30
|
+
/** How a kept width matches one the site declared. */
|
|
31
|
+
type BreakpointRounding = "nearest" | "down" | "up";
|
|
32
|
+
|
|
33
|
+
/** The most breakpoints the control will offer to keep — `@jxsuite/import`'s own ceiling. */
|
|
34
|
+
const MAX_BREAKPOINTS = 12;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The deepest crawl the pipeline accepts.
|
|
38
|
+
*
|
|
39
|
+
* The field said 2 while `import_site` and the endpoint both clamped to 5, so the wizard could not
|
|
40
|
+
* ask for a depth the backend was willing to run.
|
|
41
|
+
*/
|
|
42
|
+
const MAX_DEPTH = 5;
|
|
20
43
|
|
|
21
44
|
/** Structural view of src/ui/ai-credentials-form's controller (what this tab renders when gated). */
|
|
22
45
|
export interface CredsFormLike {
|
|
@@ -35,13 +58,16 @@ export interface ImportTabCtx {
|
|
|
35
58
|
form: { name: string; directory: string };
|
|
36
59
|
/**
|
|
37
60
|
* Validate the shared Parameters fields and return where to write, or null when something is
|
|
38
|
-
* missing (the modal has already rendered the reason inline).
|
|
39
|
-
*
|
|
61
|
+
* missing (the modal has already rendered the reason inline). Either destination shape: a hosted
|
|
62
|
+
* backend imports into a repository it creates, so `"repo"` reaches here too.
|
|
40
63
|
*/
|
|
41
64
|
resolveDestination: () => CreateProjectDestination | null;
|
|
42
65
|
rerender: () => void;
|
|
43
|
-
/**
|
|
44
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Called with the brief the form gathered. The modal closes WITHOUT a project — the assistant
|
|
68
|
+
* runs the import as a tool call and creates it.
|
|
69
|
+
*/
|
|
70
|
+
onHandoff: (brief: ImportBrief) => void;
|
|
45
71
|
/** The modal's shared credentials-form instance, rendered while the gate is closed. */
|
|
46
72
|
credsForm: CredsFormLike;
|
|
47
73
|
/**
|
|
@@ -56,11 +82,55 @@ export interface ImportTabCtx {
|
|
|
56
82
|
let _url = "";
|
|
57
83
|
let _depth = 1;
|
|
58
84
|
let _maxPages = 20;
|
|
85
|
+
/**
|
|
86
|
+
* How many of the site's breakpoints the project keeps, and how a kept width matches a declared
|
|
87
|
+
* one.
|
|
88
|
+
*
|
|
89
|
+
* Three by default, and that default is the point of the control. A real site declares as many
|
|
90
|
+
* breakpoints as it has accumulated frameworks — nine is ordinary — and the importer used to keep
|
|
91
|
+
* every one, so an imported project opened with nine canvas sizes and nine columns in every style
|
|
92
|
+
* editor. Nobody authors against nine, and nobody chose these nine.
|
|
93
|
+
*/
|
|
94
|
+
let _breakpointMode: BreakpointMode = "limit";
|
|
95
|
+
let _breakpointCount = 3;
|
|
96
|
+
let _breakpointWidths = "";
|
|
97
|
+
let _breakpointRounding: BreakpointRounding = "nearest";
|
|
59
98
|
let _aiNaming = true;
|
|
60
|
-
|
|
61
|
-
|
|
99
|
+
/**
|
|
100
|
+
* The model this import will run its AI passes on. Empty means "whatever the assistant would use",
|
|
101
|
+
* which is what the tab did silently before there was a picker.
|
|
102
|
+
*
|
|
103
|
+
* A DRAFT, not the `jx.ai.model` preference: choosing a model for one import must not retarget the
|
|
104
|
+
* assistant for every later conversation. `startImport` falls back to {@link preferredModel} so an
|
|
105
|
+
* untouched picker behaves exactly as the tab did before.
|
|
106
|
+
*/
|
|
107
|
+
let _model = "";
|
|
108
|
+
/** What the user wants done with the site once it is imported. Handed to the assistant. */
|
|
109
|
+
let _prompt = "";
|
|
110
|
+
/**
|
|
111
|
+
* Build the result and screenshot-diff it against the original.
|
|
112
|
+
*
|
|
113
|
+
* Off by default because it roughly doubles the run: a full compile plus a second browser pass.
|
|
114
|
+
* Worth offering because it produces the only finding that says how WELL the clone came out — every
|
|
115
|
+
* other one is a count of things that were skipped, and a page at 61% fidelity is a question the
|
|
116
|
+
* assistant can put to a person.
|
|
117
|
+
*/
|
|
118
|
+
let _verify = false;
|
|
119
|
+
/**
|
|
120
|
+
* The average fidelity the clone has to reach before the run counts as having matched the original.
|
|
121
|
+
*
|
|
122
|
+
* A floor, not a quality target: a faithful import of a complicated site lands well under 100 for
|
|
123
|
+
* reasons no importer can fix (a rotating hero, a font rendering a hair differently). What it
|
|
124
|
+
* catches is the other case — the clone that came out at 8% and reported success anyway. Same
|
|
125
|
+
* default as `jx-import --min-fidelity`, so the two surfaces answer the same question the same
|
|
126
|
+
* way.
|
|
127
|
+
*
|
|
128
|
+
* Failing it is a FINDING here rather than a failure: the project is written and opened either way,
|
|
129
|
+
* and the assistant reports that the bar was missed. The CLI turns the same number into an exit
|
|
130
|
+
* code because a script in a pipeline has nobody to tell.
|
|
131
|
+
*/
|
|
132
|
+
let _minFidelity = 25;
|
|
62
133
|
let _errorMsg = "";
|
|
63
|
-
let _abort: AbortController | null = null;
|
|
64
134
|
let _dirManual = false;
|
|
65
135
|
|
|
66
136
|
/** Reset the tab's state when the modal opens. */
|
|
@@ -68,14 +138,85 @@ export function resetImportTab() {
|
|
|
68
138
|
_url = "";
|
|
69
139
|
_depth = 1;
|
|
70
140
|
_maxPages = 20;
|
|
141
|
+
_breakpointMode = "limit";
|
|
142
|
+
_breakpointCount = 3;
|
|
143
|
+
_breakpointWidths = "";
|
|
144
|
+
_breakpointRounding = "nearest";
|
|
71
145
|
_aiNaming = true;
|
|
72
|
-
|
|
73
|
-
|
|
146
|
+
_model = "";
|
|
147
|
+
_prompt = "";
|
|
148
|
+
_verify = false;
|
|
149
|
+
_minFidelity = 25;
|
|
74
150
|
_errorMsg = "";
|
|
75
|
-
_abort = null;
|
|
76
151
|
_dirManual = false;
|
|
77
152
|
}
|
|
78
153
|
|
|
154
|
+
/**
|
|
155
|
+
* The tab's model picker, and the scheduler it repaints through.
|
|
156
|
+
*
|
|
157
|
+
* Created once and kept: `createModelPicker` holds the in-flight fetch and the failed-connection
|
|
158
|
+
* record, and a picker rebuilt per render would re-fetch the catalogue on every keystroke. The
|
|
159
|
+
* scheduler is a SLOT rather than a captured closure because `ImportTabCtx` is rebuilt per render
|
|
160
|
+
* (see `importCtxFor`), so the picker must reach whichever one is current.
|
|
161
|
+
*/
|
|
162
|
+
let _rerender: (() => void) | null = null;
|
|
163
|
+
let _picker: ReturnType<typeof createModelPicker> | null = null;
|
|
164
|
+
|
|
165
|
+
function modelPicker() {
|
|
166
|
+
_picker ??= createModelPicker({
|
|
167
|
+
className: "new-project-import-model",
|
|
168
|
+
getModel: () => _model || preferredModel(),
|
|
169
|
+
// A draft, deliberately not `setModel` — see `_model`.
|
|
170
|
+
onChange: (id) => {
|
|
171
|
+
_model = id;
|
|
172
|
+
},
|
|
173
|
+
requestRender: () => _rerender?.(),
|
|
174
|
+
});
|
|
175
|
+
return _picker;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The policy the three controls describe.
|
|
180
|
+
*
|
|
181
|
+
* Exported so a test can read what the form would send without driving the assistant, for the same
|
|
182
|
+
* reason {@link importBriefFor} is.
|
|
183
|
+
*
|
|
184
|
+
* @returns {ImportBreakpointPolicy}
|
|
185
|
+
*/
|
|
186
|
+
export function breakpointPolicyFromForm(): ImportBreakpointPolicy {
|
|
187
|
+
if (_breakpointMode === "all") {
|
|
188
|
+
return { mode: "all" };
|
|
189
|
+
}
|
|
190
|
+
if (_breakpointMode === "explicit") {
|
|
191
|
+
const widths = _breakpointWidths
|
|
192
|
+
.split(",")
|
|
193
|
+
.map((part) => Math.trunc(Number(part.trim())))
|
|
194
|
+
.filter((width) => Number.isFinite(width) && width > 0);
|
|
195
|
+
/* A half-typed width list is not an instruction to keep nothing. Until it parses into at least
|
|
196
|
+
one width, the form still means "three, evenly spaced" — the value the field started from. */
|
|
197
|
+
if (widths.length > 0) {
|
|
198
|
+
return { mode: "explicit", rounding: _breakpointRounding, widths };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { count: _breakpointCount, mode: "limit", rounding: _breakpointRounding };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Whether this backend can answer the fidelity question at all.
|
|
206
|
+
*
|
|
207
|
+
* `verify` builds the emitted project with the compiler and serves it to screenshot every page
|
|
208
|
+
* against the original — it executes the project's own JavaScript, so `@jxsuite/import` keeps that
|
|
209
|
+
* phase out of the portable pipeline entirely and a hosted backend deliberately never runs it.
|
|
210
|
+
*
|
|
211
|
+
* There is no PAL member for "this backend compiles", so the nearest DECLARED fact stands in:
|
|
212
|
+
* `createDestination`. A `"repo"` platform commits the emitted project into a git tree it never
|
|
213
|
+
* checks out, which is the same set of hosts and one the tab can read without naming a platform by
|
|
214
|
+
* id. Offering the switch there would collect an answer nothing acts on.
|
|
215
|
+
*/
|
|
216
|
+
function canVerify(): boolean {
|
|
217
|
+
return getPlatform().createDestination !== "repo";
|
|
218
|
+
}
|
|
219
|
+
|
|
79
220
|
function slugOf(name: string): string {
|
|
80
221
|
return name
|
|
81
222
|
.toLowerCase()
|
|
@@ -83,19 +224,6 @@ function slugOf(name: string): string {
|
|
|
83
224
|
.replaceAll(/^-|-$/g, "");
|
|
84
225
|
}
|
|
85
226
|
|
|
86
|
-
/** True while an import stream is active (the modal disables tab switching and close). */
|
|
87
|
-
export function isImportRunning(): boolean {
|
|
88
|
-
return _status === "running";
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Abort the running import (the footer's Cancel Import button). */
|
|
92
|
-
export function cancelImport(ctx: ImportTabCtx) {
|
|
93
|
-
_abort?.abort();
|
|
94
|
-
_abort = null;
|
|
95
|
-
_status = "idle";
|
|
96
|
-
ctx.rerender();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
227
|
/**
|
|
100
228
|
* Validate the source step (the URL) before advancing to the Parameters step. Sets the inline error
|
|
101
229
|
* and returns false when invalid.
|
|
@@ -116,89 +244,79 @@ export function validateImportSource(ctx: ImportTabCtx): boolean {
|
|
|
116
244
|
return true;
|
|
117
245
|
}
|
|
118
246
|
|
|
119
|
-
/**
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
247
|
+
/**
|
|
248
|
+
* The brief this form has gathered, or null when something is still missing (the modal has already
|
|
249
|
+
* rendered the reason inline).
|
|
250
|
+
*
|
|
251
|
+
* Exported so a test can read what the form would hand over without driving the assistant.
|
|
252
|
+
*
|
|
253
|
+
* @param {ImportTabCtx} ctx
|
|
254
|
+
* @returns {ImportBrief | null}
|
|
255
|
+
*/
|
|
256
|
+
export function importBriefFor(ctx: ImportTabCtx): ImportBrief | null {
|
|
125
257
|
let parsed: URL;
|
|
126
258
|
try {
|
|
127
259
|
parsed = new URL(_url.trim());
|
|
128
260
|
} catch {
|
|
129
261
|
_errorMsg = "Enter a valid URL (e.g. https://example.com)";
|
|
130
262
|
ctx.rerender();
|
|
131
|
-
return;
|
|
263
|
+
return null;
|
|
132
264
|
}
|
|
133
265
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
134
266
|
_errorMsg = "The URL must start with http:// or https://";
|
|
135
267
|
ctx.rerender();
|
|
136
|
-
return;
|
|
268
|
+
return null;
|
|
137
269
|
}
|
|
138
270
|
const destination = ctx.resolveDestination();
|
|
139
|
-
if (!destination
|
|
140
|
-
return;
|
|
271
|
+
if (!destination) {
|
|
272
|
+
return null;
|
|
141
273
|
}
|
|
274
|
+
return {
|
|
275
|
+
aiComponents: _aiNaming,
|
|
276
|
+
breakpoints: breakpointPolicyFromForm(),
|
|
277
|
+
depth: _depth,
|
|
278
|
+
directory: destinationPath(destination, ctx.form.directory),
|
|
279
|
+
maxPages: _maxPages,
|
|
280
|
+
model: _model,
|
|
281
|
+
name: ctx.form.name.trim(),
|
|
282
|
+
prompt: _prompt.trim(),
|
|
283
|
+
minFidelity: _minFidelity,
|
|
284
|
+
url: parsed.href,
|
|
285
|
+
/* Never asked for where it cannot run: the switch is not rendered there, and a value left over
|
|
286
|
+
from a session on another platform must not travel as a request the backend would ignore. */
|
|
287
|
+
verify: canVerify() && _verify,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
142
290
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
directory: destinationPath(destination, ctx.form.directory),
|
|
158
|
-
depth: _depth,
|
|
159
|
-
maxPages: _maxPages,
|
|
160
|
-
aiComponents: _aiNaming,
|
|
161
|
-
...(key ? { apiKey: key } : {}),
|
|
162
|
-
...(baseUrl ? { baseUrl } : {}),
|
|
163
|
-
...(model ? { model } : {}),
|
|
164
|
-
},
|
|
165
|
-
(evt) => {
|
|
166
|
-
_log = [..._log, evt];
|
|
167
|
-
// The log is destroyed the moment a successful import hands off, so the one line the client
|
|
168
|
-
// Emits about lines it could not read is re-posted where it will outlive that.
|
|
169
|
-
if (evt.phase === IMPORT_WARNING_PHASE) {
|
|
170
|
-
notify.warn(evt.message);
|
|
171
|
-
}
|
|
172
|
-
ctx.rerender();
|
|
173
|
-
requestAnimationFrame(() => {
|
|
174
|
-
const el = document.querySelector(".new-project-import-log");
|
|
175
|
-
if (el) {
|
|
176
|
-
el.scrollTop = el.scrollHeight;
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
},
|
|
180
|
-
_abort.signal,
|
|
181
|
-
);
|
|
182
|
-
_status = "idle";
|
|
183
|
-
_abort = null;
|
|
184
|
-
ctx.onDone(result);
|
|
185
|
-
} catch (error) {
|
|
186
|
-
if (_status !== "running") {
|
|
187
|
-
return; // Cancelled by the user — cancelImport already reset the tab.
|
|
188
|
-
}
|
|
189
|
-
_abort = null;
|
|
190
|
-
_status = "error";
|
|
191
|
-
_errorMsg = errorMessage(error);
|
|
192
|
-
ctx.rerender();
|
|
291
|
+
/**
|
|
292
|
+
* Hand the brief to the assistant and close the wizard. Wired to the modal footer's primary button.
|
|
293
|
+
*
|
|
294
|
+
* **The wizard does not run the import any more.** It used to, and the run's whole account of
|
|
295
|
+
* itself — every phase line, every warning — was destroyed the moment it succeeded and the modal
|
|
296
|
+
* handed off. It is a tool call now (`services/ai-import-tools.ts`), so the progress lives in the
|
|
297
|
+
* transcript, the model can read what the crawl actually found, and it can stop and ask about
|
|
298
|
+
* anything the pipeline had to guess at.
|
|
299
|
+
*
|
|
300
|
+
* @param {ImportTabCtx} ctx
|
|
301
|
+
*/
|
|
302
|
+
export function handoffImport(ctx: ImportTabCtx) {
|
|
303
|
+
if (!getPlatform().importSite) {
|
|
304
|
+
return;
|
|
193
305
|
}
|
|
306
|
+
const brief = importBriefFor(ctx);
|
|
307
|
+
if (!brief) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
/* Stored HERE, by the form that gathered it, rather than inside the assistant's hand-off: two
|
|
311
|
+
readers need it — the turn that gets composed now, and `import_site` minutes later for the
|
|
312
|
+
destination — and the producer is the one place both can be sure it was written. */
|
|
313
|
+
setPendingImportBrief(brief);
|
|
314
|
+
ctx.onHandoff(brief);
|
|
194
315
|
}
|
|
195
316
|
|
|
196
|
-
/** The footer's primary-button label
|
|
317
|
+
/** The footer's primary-button label. One state: the wizard hands off and closes. */
|
|
197
318
|
export function importButtonLabel(): string {
|
|
198
|
-
|
|
199
|
-
return "Importing…";
|
|
200
|
-
}
|
|
201
|
-
return _status === "error" ? "Retry Import" : "Import Site";
|
|
319
|
+
return "Import Site";
|
|
202
320
|
}
|
|
203
321
|
|
|
204
322
|
function onUrlInput(ctx: ImportTabCtx) {
|
|
@@ -222,59 +340,96 @@ function onUrlInput(ctx: ImportTabCtx) {
|
|
|
222
340
|
};
|
|
223
341
|
}
|
|
224
342
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
)}
|
|
242
|
-
</div>
|
|
243
|
-
`;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** The streaming progress view shown (in place of the Parameters step) while a run is active. */
|
|
247
|
-
export function renderImportProgress(): TemplateResult {
|
|
248
|
-
return renderProgress();
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/** Inline error + retained log, rendered on the Parameters step after a failed run. */
|
|
252
|
-
export function renderImportStatus(): TemplateResult {
|
|
253
|
-
if (_status !== "error") {
|
|
254
|
-
return html``;
|
|
255
|
-
}
|
|
343
|
+
/**
|
|
344
|
+
* The Breakpoints control: how many of the site's own breakpoints the project ends up with.
|
|
345
|
+
*
|
|
346
|
+
* Three controls rather than one because they answer different questions and only some of them
|
|
347
|
+
* apply at a time — a count is meaningless once you have named the widths, and both are meaningless
|
|
348
|
+
* when you are keeping everything. The rounding picker stays for the two that use it: it decides
|
|
349
|
+
* which DECLARED width backs a kept one, and the styles flip where the site says they do.
|
|
350
|
+
*
|
|
351
|
+
* @param {ImportTabCtx} ctx
|
|
352
|
+
* @returns {TemplateResult}
|
|
353
|
+
*/
|
|
354
|
+
function breakpointsField(ctx: ImportTabCtx): TemplateResult {
|
|
355
|
+
const onMode = (e: Event) => {
|
|
356
|
+
_breakpointMode = (e.target as HTMLInputElement).value as BreakpointMode;
|
|
357
|
+
ctx.rerender();
|
|
358
|
+
};
|
|
256
359
|
return html`
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
360
|
+
<label class="new-project-field">
|
|
361
|
+
<span class="new-project-label">Breakpoints</span>
|
|
362
|
+
<div class="new-project-import-breakpoints">
|
|
363
|
+
<sp-picker
|
|
364
|
+
size="m"
|
|
365
|
+
class="new-project-breakpoint-mode"
|
|
366
|
+
label="Breakpoints"
|
|
367
|
+
.value=${live(_breakpointMode)}
|
|
368
|
+
@change=${onMode}
|
|
369
|
+
>
|
|
370
|
+
<sp-menu-item value="limit">Limit to</sp-menu-item>
|
|
371
|
+
<sp-menu-item value="explicit">Custom widths</sp-menu-item>
|
|
372
|
+
<sp-menu-item value="all">Keep all</sp-menu-item>
|
|
373
|
+
</sp-picker>
|
|
374
|
+
${
|
|
375
|
+
_breakpointMode === "limit"
|
|
376
|
+
? html`<sp-number-field
|
|
377
|
+
class="new-project-breakpoint-count"
|
|
378
|
+
min="1"
|
|
379
|
+
max=${MAX_BREAKPOINTS}
|
|
380
|
+
.value=${live(_breakpointCount)}
|
|
381
|
+
@change=${(e: Event) => {
|
|
382
|
+
_breakpointCount = Math.trunc(Number((e.target as HTMLInputElement).value)) || 3;
|
|
383
|
+
}}
|
|
384
|
+
></sp-number-field>`
|
|
385
|
+
: nothing
|
|
386
|
+
}
|
|
387
|
+
${
|
|
388
|
+
_breakpointMode === "explicit"
|
|
389
|
+
? html`<sp-textfield
|
|
390
|
+
class="new-project-breakpoint-widths"
|
|
391
|
+
placeholder="640, 1024, 1440"
|
|
392
|
+
.value=${live(_breakpointWidths)}
|
|
393
|
+
@input=${(e: Event) => {
|
|
394
|
+
_breakpointWidths = (e.target as HTMLInputElement).value;
|
|
395
|
+
}}
|
|
396
|
+
></sp-textfield>`
|
|
397
|
+
: nothing
|
|
398
|
+
}
|
|
399
|
+
${
|
|
400
|
+
_breakpointMode === "all"
|
|
401
|
+
? nothing
|
|
402
|
+
: html`<sp-picker
|
|
403
|
+
size="m"
|
|
404
|
+
class="new-project-breakpoint-rounding"
|
|
405
|
+
label="Rounding"
|
|
406
|
+
.value=${live(_breakpointRounding)}
|
|
407
|
+
@change=${(e: Event) => {
|
|
408
|
+
_breakpointRounding = (e.target as HTMLInputElement).value as BreakpointRounding;
|
|
409
|
+
}}
|
|
410
|
+
>
|
|
411
|
+
<sp-menu-item value="nearest">nearest</sp-menu-item>
|
|
412
|
+
<sp-menu-item value="down">round down</sp-menu-item>
|
|
413
|
+
<sp-menu-item value="up">round up</sp-menu-item>
|
|
414
|
+
</sp-picker>`
|
|
415
|
+
}
|
|
416
|
+
</div>
|
|
417
|
+
<span class="new-project-hint">
|
|
418
|
+
${
|
|
419
|
+
_breakpointMode === "all"
|
|
420
|
+
? "Every breakpoint the site declares becomes a canvas size. Real sites often declare nine."
|
|
421
|
+
: _breakpointMode === "explicit"
|
|
422
|
+
? "The project gets these widths, each backed by the declared width nearest it."
|
|
423
|
+
: "Evenly spaced across the widths the site declares — the narrowest, the widest, and the ones between."
|
|
424
|
+
}
|
|
425
|
+
</span>
|
|
426
|
+
</label>
|
|
274
427
|
`;
|
|
275
428
|
}
|
|
276
429
|
|
|
277
430
|
export function renderImportSource(ctx: ImportTabCtx): TemplateResult {
|
|
431
|
+
// `ctx` is rebuilt per render, so the picker's scheduler has to be re-pointed at the current one.
|
|
432
|
+
_rerender = ctx.rerender;
|
|
278
433
|
if (!ctx.aiGateOpen()) {
|
|
279
434
|
return html`
|
|
280
435
|
<div class="new-project-tab-intro">
|
|
@@ -297,7 +452,7 @@ export function renderImportSource(ctx: ImportTabCtx): TemplateResult {
|
|
|
297
452
|
<span class="new-project-label">Site URL *</span>
|
|
298
453
|
<sp-textfield
|
|
299
454
|
placeholder="https://example.com"
|
|
300
|
-
.value=${_url}
|
|
455
|
+
.value=${live(_url)}
|
|
301
456
|
@input=${onUrlInput(ctx)}
|
|
302
457
|
style="width: 100%"
|
|
303
458
|
></sp-textfield>
|
|
@@ -307,8 +462,8 @@ export function renderImportSource(ctx: ImportTabCtx): TemplateResult {
|
|
|
307
462
|
<span class="new-project-label">Crawl Depth</span>
|
|
308
463
|
<sp-number-field
|
|
309
464
|
min="0"
|
|
310
|
-
max
|
|
311
|
-
.value=${_depth}
|
|
465
|
+
max=${MAX_DEPTH}
|
|
466
|
+
.value=${live(_depth)}
|
|
312
467
|
@change=${(e: Event) => {
|
|
313
468
|
_depth = Math.trunc(Number((e.target as HTMLInputElement).value)) || 0;
|
|
314
469
|
}}
|
|
@@ -319,37 +474,76 @@ export function renderImportSource(ctx: ImportTabCtx): TemplateResult {
|
|
|
319
474
|
<sp-number-field
|
|
320
475
|
min="1"
|
|
321
476
|
max="100"
|
|
322
|
-
.value=${_maxPages}
|
|
477
|
+
.value=${live(_maxPages)}
|
|
323
478
|
@change=${(e: Event) => {
|
|
324
479
|
_maxPages = Math.trunc(Number((e.target as HTMLInputElement).value)) || 1;
|
|
325
480
|
}}
|
|
326
481
|
></sp-number-field>
|
|
327
482
|
</label>
|
|
328
483
|
</div>
|
|
484
|
+
${breakpointsField(ctx)}
|
|
329
485
|
<sp-switch
|
|
330
|
-
|
|
486
|
+
.checked=${live(_aiNaming)}
|
|
331
487
|
@change=${(e: Event) => {
|
|
332
488
|
_aiNaming = (e.target as HTMLInputElement).checked;
|
|
333
489
|
}}
|
|
334
490
|
>
|
|
335
491
|
AI component naming
|
|
336
492
|
</sp-switch>
|
|
337
|
-
${_errorMsg ? html`<div class="new-project-error">${_errorMsg}</div>` : ""}
|
|
338
493
|
${
|
|
339
|
-
|
|
494
|
+
canVerify()
|
|
495
|
+
? html`<sp-switch
|
|
496
|
+
.checked=${live(_verify)}
|
|
497
|
+
@change=${(e: Event) => {
|
|
498
|
+
_verify = (e.target as HTMLInputElement).checked;
|
|
499
|
+
// The bar below it only means anything while the check runs, so it appears with it.
|
|
500
|
+
ctx.rerender();
|
|
501
|
+
}}
|
|
502
|
+
>
|
|
503
|
+
Check fidelity against the original (slower)
|
|
504
|
+
</sp-switch>`
|
|
505
|
+
: nothing
|
|
506
|
+
}
|
|
507
|
+
${
|
|
508
|
+
canVerify() && _verify
|
|
340
509
|
? html`
|
|
341
|
-
<
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
510
|
+
<label class="new-project-field">
|
|
511
|
+
<span class="new-project-label">Minimum fidelity</span>
|
|
512
|
+
<sp-number-field
|
|
513
|
+
class="new-project-min-fidelity"
|
|
514
|
+
min="0"
|
|
515
|
+
max="100"
|
|
516
|
+
.value=${live(_minFidelity)}
|
|
517
|
+
@change=${(e: Event) => {
|
|
518
|
+
const raw = Math.trunc(Number((e.target as HTMLInputElement).value));
|
|
519
|
+
_minFidelity = Number.isNaN(raw) ? 0 : Math.min(100, Math.max(0, raw));
|
|
520
|
+
}}
|
|
521
|
+
></sp-number-field>
|
|
522
|
+
<span class="new-project-hint">
|
|
523
|
+
Below this average, the assistant reports that the clone did not match the original.
|
|
524
|
+
A floor rather than a target: a faithful import of a complicated site still lands
|
|
525
|
+
well under 100. Set 0 to report the score without judging it.
|
|
526
|
+
</span>
|
|
527
|
+
</label>
|
|
351
528
|
`
|
|
352
529
|
: ""
|
|
353
530
|
}
|
|
531
|
+
<label class="new-project-field">
|
|
532
|
+
<span class="new-project-label">Model</span>
|
|
533
|
+
${modelPicker().render()}
|
|
534
|
+
</label>
|
|
535
|
+
<label class="new-project-field">
|
|
536
|
+
<span class="new-project-label">What should the assistant do with it?</span>
|
|
537
|
+
<sp-textfield
|
|
538
|
+
multiline
|
|
539
|
+
class="new-project-import-prompt"
|
|
540
|
+
placeholder="Keep the layout but modernise the typography, and turn the news list into a content collection…"
|
|
541
|
+
.value=${live(_prompt)}
|
|
542
|
+
@input=${(e: Event) => {
|
|
543
|
+
_prompt = (e.target as HTMLInputElement).value;
|
|
544
|
+
}}
|
|
545
|
+
></sp-textfield>
|
|
546
|
+
</label>
|
|
547
|
+
${_errorMsg ? html`<div class="new-project-error">${_errorMsg}</div>` : ""}
|
|
354
548
|
`;
|
|
355
549
|
}
|
|
@@ -140,13 +140,16 @@ function joinPath(parent: string, name: string): string {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
|
-
* The
|
|
144
|
-
*
|
|
145
|
-
*
|
|
143
|
+
* The destination as the import pipeline names it: `importSite` takes a plain `directory` string
|
|
144
|
+
* rather than a destination object, so the modal flattens one here.
|
|
145
|
+
*
|
|
146
|
+
* Both shapes, because both platforms import now. A `"path"` destination flattens to the absolute
|
|
147
|
+
* folder it writes; a `"repo"` one to `owner/repo`, which is what a backend that commits the
|
|
148
|
+
* emitted project into a git tree can act on — there is no directory for it to name.
|
|
146
149
|
*/
|
|
147
150
|
export function destinationPath(destination: CreateProjectDestination, slug: string): string {
|
|
148
|
-
if (destination.kind
|
|
149
|
-
|
|
151
|
+
if (destination.kind === "repo") {
|
|
152
|
+
return `${destination.owner}/${destination.repo || slug.trim()}`;
|
|
150
153
|
}
|
|
151
154
|
return joinPath(destination.parent, slug.trim());
|
|
152
155
|
}
|