@hazem3500/palette 0.9.108 → 0.9.110
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/NODE.md +32 -30
- package/README.md +58 -46
- package/cli/agent.mjs +93 -69
- package/cli/config.mjs +17 -1
- package/cli/design-edit.mjs +9 -7
- package/cli/index.mjs +26 -24
- package/cli/snapshots.mjs +3 -3
- package/cli/workflow.mjs +15 -7
- package/dist/{accent-picker-TTIZ65XF.js → accent-picker-PPVPURA7.js} +1 -1
- package/dist/account-tokens-LBRVZAEJ.js +1 -0
- package/dist/adapters/css-variables.js +1 -1
- package/dist/adapters/tailwind.js +1 -1
- package/dist/chunk-2FX2JEMO.js +1 -0
- package/dist/chunk-4HBBEVFK.js +1 -0
- package/dist/chunk-5HZI7HRR.js +1 -0
- package/dist/chunk-DU7H4TEX.js +1 -0
- package/dist/chunk-FGZXQUAD.js +1 -0
- package/dist/{chunk-AYGLN3MI.js → chunk-HLXSRXTK.js} +1 -1
- package/dist/{chunk-DZRM3YUQ.js → chunk-J47R52WX.js} +1 -1
- package/dist/chunk-KELFDOEC.js +1 -0
- package/dist/{chunk-2J3IID4N.js → chunk-KLPQTFVH.js} +1 -1
- package/dist/chunk-ONUORDYE.js +1 -0
- package/dist/chunk-PF4NYFZK.js +1 -0
- package/dist/chunk-S6NEYT36.js +1 -0
- package/dist/chunk-U55ASP2Y.js +1 -0
- package/dist/chunk-VX6XCU4E.js +1 -0
- package/dist/color-editor-QPJQD36F.js +1 -0
- package/dist/config.d.ts +1 -1
- package/dist/design-comment-BF2KGPWU.js +1 -0
- package/dist/{design-export-BNNHI4JP.js → design-export-Y2GU3KER.js} +1 -1
- package/dist/design-geometry-HJLEXLPO.js +1 -0
- package/dist/design-mode-styles.d.ts +1 -1
- package/dist/design-mode.js +1 -1
- package/dist/design-panel-ZQS4WQCI.js +1 -0
- package/dist/design-panel-styles.d.ts +1 -1
- package/dist/design-popup.d.ts +1 -1
- package/dist/design-system.d.ts +5 -4
- package/dist/{field-controls-4ROTPXMO.js → field-controls-4AIIXPNQ.js} +1 -1
- package/dist/{frame-iframe-WBEP3R6S.js → frame-iframe-PYLYMCOK.js} +1 -1
- package/dist/frame-size-TF6SIUFO.js +1 -0
- package/dist/frame-target.d.ts +5 -2
- package/dist/host-motion.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/inline-text-editor-MK6FHDAM.js +1 -0
- package/dist/live-colors-D426RBEC.js +1 -0
- package/dist/live-colors.d.ts +1 -1
- package/dist/node.mjs +102 -75
- package/dist/onboarding-6OSADOWE.js +1 -0
- package/dist/{pin-groups-YAYFPJYA.js → pin-groups-GF53RTHP.js} +1 -1
- package/dist/{pin-move-PG54WWPU.js → pin-move-G6EWHLDO.js} +1 -1
- package/dist/public-env.d.ts +49 -11
- package/dist/react-props-panel-ZSKQ44GS.js +1 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +1 -1
- package/dist/select-menu-OZVNHZ73.js +1 -0
- package/dist/server.js +80 -57
- package/dist/setup.js +1 -1
- package/dist/shell-mode-PLV6F474.js +1 -0
- package/dist/shell-session.d.ts +5 -3
- package/dist/shell.d.ts +1 -1
- package/dist/shell.js +1 -1
- package/dist/shortcut-help-DIMOT2ZK.js +1 -0
- package/dist/snapshot-3PSQKXUV.js +1 -0
- package/dist/snapshot.d.ts +1 -1
- package/dist/source-info.d.ts +1 -1
- package/dist/storage-keys.d.ts +5 -0
- package/dist/tag-editor-B3I7WD4N.js +1 -0
- package/dist/thread-snapshot-ML2RZVID.js +1 -0
- package/dist/token-custom-CASAQWYW.js +1 -0
- package/dist/token-page-NIB5HAVS.js +1 -0
- package/dist/token-picker-AJ5E5IN5.js +1 -0
- package/dist/{toolbar-runtime-QCZONSWE.js → toolbar-runtime-TZWY2KH3.js} +1 -1
- package/dist/typed-custom.d.ts +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/server/account-page.ts +6 -6
- package/server/iap.ts +2 -2
- package/server/index.ts +30 -24
- package/server/project-management.ts +5 -5
- package/server/proxy-auth.ts +17 -16
- package/server/public-env.ts +67 -11
- package/server/setup-client.txt +173 -173
- package/server/setup-page.ts +1 -1
- package/server/tokens.ts +9 -6
- package/server/webhooks.ts +9 -5
- package/src/types.ts +1 -1
- package/dist/account-tokens-PINQNSNE.js +0 -1
- package/dist/chunk-4KEVPMJJ.js +0 -1
- package/dist/chunk-4XQD75A2.js +0 -1
- package/dist/chunk-57MG3M5P.js +0 -1
- package/dist/chunk-5OVDW2RU.js +0 -1
- package/dist/chunk-6HM4JQLV.js +0 -1
- package/dist/chunk-DLC3CRFV.js +0 -1
- package/dist/chunk-DVYSFT6O.js +0 -1
- package/dist/chunk-FD4GACV2.js +0 -1
- package/dist/chunk-IQOPM3JF.js +0 -1
- package/dist/chunk-UEZCLURX.js +0 -1
- package/dist/color-editor-ATWWSENS.js +0 -1
- package/dist/design-comment-RXUCDTYA.js +0 -1
- package/dist/design-geometry-DPMHGQEK.js +0 -1
- package/dist/design-panel-HTN6LR3V.js +0 -1
- package/dist/frame-size-MRCN6XYD.js +0 -1
- package/dist/inline-text-editor-AMFRKW5Z.js +0 -1
- package/dist/live-colors-GWOVVQSO.js +0 -1
- package/dist/onboarding-BVTCFEHL.js +0 -1
- package/dist/react-props-panel-4XO3NSX3.js +0 -1
- package/dist/select-menu-QAYV7NKJ.js +0 -1
- package/dist/shell-mode-SUYLWDWW.js +0 -1
- package/dist/shortcut-help-XRDNX7MZ.js +0 -1
- package/dist/snapshot-V264ZMQG.js +0 -1
- package/dist/tag-editor-MMAMOEI4.js +0 -1
- package/dist/thread-snapshot-HLTNHS4N.js +0 -1
- package/dist/token-custom-UCA6CBOV.js +0 -1
- package/dist/token-page-PGPWPUKU.js +0 -1
- package/dist/token-picker-WMWUKLKJ.js +0 -1
package/cli/agent.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { homedir } from "node:os";
|
|
|
14
14
|
import { createHash, randomBytes } from "node:crypto";
|
|
15
15
|
import { createServer } from "node:http";
|
|
16
16
|
import { spawn } from "node:child_process";
|
|
17
|
-
import { branchName } from "./config.mjs";
|
|
17
|
+
import { branchName, setting } from "./config.mjs";
|
|
18
18
|
import { agentWorkflow, agentPrompt } from "../dist/agent-prompt.js";
|
|
19
19
|
import { designEditFromComment, formatDesignEdit } from "./design-edit.mjs";
|
|
20
20
|
|
|
@@ -26,7 +26,7 @@ const actions = {
|
|
|
26
26
|
untag: "Remove a group tag: untag THREAD_ID TAG",
|
|
27
27
|
get: "Read one complete thread: get THREAD_ID; --snapshots also saves its element snapshots as files",
|
|
28
28
|
prompt: "Export open feedback as Markdown; --page /path, --json, --snapshots to save element snapshots and list their files",
|
|
29
|
-
snapshots: "Save a thread's element snapshots as files: snapshots THREAD_ID [--out DIR] (default .komo/snapshots)",
|
|
29
|
+
snapshots: "Save a thread's element snapshots as files: snapshots THREAD_ID [--out DIR] (default .palette/snapshots, or .komo/snapshots in an older project)",
|
|
30
30
|
create: "Create a thread: --page /path --anchor-file FILE --body TEXT",
|
|
31
31
|
reply: "Reply to a thread: reply THREAD_ID --body TEXT",
|
|
32
32
|
resolve: "Resolve a thread: resolve THREAD_ID",
|
|
@@ -70,10 +70,10 @@ ${Object.entries(actions)
|
|
|
70
70
|
.map(([name, description]) => ` ${name}: ${description}`)
|
|
71
71
|
.join("\n")}
|
|
72
72
|
|
|
73
|
-
Settings: .komo/project.json (searched upward), or --project KEY.
|
|
73
|
+
Settings: .palette/project.json, or .komo/project.json from before the rename (searched upward), or --project KEY.
|
|
74
74
|
Overrides: --endpoint URL --origin SITE_URL --repo OWNER/REPO --branch NAME
|
|
75
75
|
Bodies: --body TEXT, --body-file FILE, or --body-file - for stdin.
|
|
76
|
-
Credentials: KOMO_TOKEN, then a saved access token, then a Google login session. Never commit tokens.
|
|
76
|
+
Credentials: PALETTE_TOKEN (or the older KOMO_TOKEN), then a saved access token, then a Google login session. Never commit tokens.
|
|
77
77
|
Create access tokens at /account on your Palette server; pass one with: printf %s "$TOKEN" | palette login --token
|
|
78
78
|
Results are JSON except prompt (Markdown unless --json). Errors are JSON on stderr.
|
|
79
79
|
Design mode messages also carry designEdit: a parsed Palette DesignEdit v1 record.
|
|
@@ -143,39 +143,44 @@ function safeUrl(value, originOnly = false) {
|
|
|
143
143
|
async function configuration(flags, cwd, env) {
|
|
144
144
|
let directory = cwd,
|
|
145
145
|
settings = {},
|
|
146
|
-
root = cwd
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
146
|
+
root = cwd,
|
|
147
|
+
settingsDir = ".palette";
|
|
148
|
+
search: while (true) {
|
|
149
|
+
// .komo/ is where a project set up before the rename keeps its settings.
|
|
150
|
+
for (const dir of [".palette", ".komo"])
|
|
151
|
+
try {
|
|
152
|
+
settings = JSON.parse(
|
|
153
|
+
await readFile(join(directory, dir, "project.json"), "utf8")
|
|
154
|
+
);
|
|
155
|
+
root = directory;
|
|
156
|
+
settingsDir = dir;
|
|
157
|
+
break search;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error.code !== "ENOENT") throw error;
|
|
160
|
+
}
|
|
157
161
|
if (dirname(directory) === directory) break;
|
|
158
162
|
directory = dirname(directory);
|
|
159
163
|
}
|
|
160
|
-
const project = flags.project || env
|
|
164
|
+
const project = flags.project || setting(env, "PROJECT") || settings.project;
|
|
161
165
|
if (!project)
|
|
162
166
|
throw Error("Run palette init in this repository, or pass --project KEY.");
|
|
163
|
-
const
|
|
164
|
-
flags.endpoint ||
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
"
|
|
168
|
-
|
|
167
|
+
const configured =
|
|
168
|
+
flags.endpoint || setting(env, "ENDPOINT") || settings.endpoint;
|
|
169
|
+
if (!configured)
|
|
170
|
+
throw Error(
|
|
171
|
+
"No Palette server configured. Pass --endpoint URL, set PALETTE_ENDPOINT, or add endpoint to .palette/project.json."
|
|
172
|
+
);
|
|
173
|
+
const endpoint = safeUrl(configured);
|
|
169
174
|
const origin = safeUrl(
|
|
170
175
|
flags.origin ||
|
|
171
|
-
env
|
|
176
|
+
setting(env, "ORIGIN") ||
|
|
172
177
|
settings.origin ||
|
|
173
178
|
"http://localhost:3000",
|
|
174
179
|
true
|
|
175
180
|
);
|
|
176
181
|
const branch =
|
|
177
182
|
flags.branch ||
|
|
178
|
-
env
|
|
183
|
+
setting(env, "BRANCH") ||
|
|
179
184
|
(settings.scope === "branch" ? branchName(env, directory) : "shared");
|
|
180
185
|
if (!branch) throw Error("Cannot detect branch. Pass --branch NAME.");
|
|
181
186
|
return {
|
|
@@ -183,42 +188,55 @@ async function configuration(flags, cwd, env) {
|
|
|
183
188
|
endpoint,
|
|
184
189
|
origin,
|
|
185
190
|
branch,
|
|
186
|
-
repo: flags.repo || env
|
|
191
|
+
repo: flags.repo || setting(env, "REPO") || settings.repo || project,
|
|
187
192
|
root,
|
|
193
|
+
settingsDir,
|
|
188
194
|
};
|
|
189
195
|
}
|
|
190
|
-
|
|
196
|
+
// Credentials are saved under ~/.config/palette (or PALETTE_CONFIG_HOME). Ones
|
|
197
|
+
// saved before the rename, under ~/.config/komo, are still read; logout removes both.
|
|
198
|
+
function credentialFiles(env, name) {
|
|
199
|
+
const home = setting(env, "CONFIG_HOME");
|
|
200
|
+
return (home ? [home] : [join(homedir(), ".config", "palette"), join(homedir(), ".config", "komo")])
|
|
201
|
+
.map((directory) => join(directory, name));
|
|
202
|
+
}
|
|
203
|
+
function credentialPaths(config, env) {
|
|
191
204
|
const key = createHash("sha256")
|
|
192
205
|
.update(`${config.endpoint}\n${config.project}`)
|
|
193
206
|
.digest("hex");
|
|
194
|
-
return
|
|
195
|
-
env.KOMO_CONFIG_HOME || join(homedir(), ".config", "komo"),
|
|
196
|
-
`${key}.json`
|
|
197
|
-
);
|
|
207
|
+
return credentialFiles(env, `${key}.json`);
|
|
198
208
|
}
|
|
199
|
-
const PERSONAL_TOKEN = /^
|
|
209
|
+
const PERSONAL_TOKEN = /^(?:palette|komo)_pat_[a-z0-9]{12}_[A-Za-z0-9_-]{43}$/;
|
|
210
|
+
const isPersonalToken = (token) => /^(?:palette|komo)_pat_/.test(token ?? "");
|
|
200
211
|
// A personal access token works for every project on one server, so it is
|
|
201
212
|
// stored per endpoint, beside the per-project login sessions.
|
|
202
|
-
function
|
|
213
|
+
function personalTokenPaths(config, env) {
|
|
203
214
|
const key = createHash("sha256").update(config.endpoint).digest("hex");
|
|
204
|
-
return
|
|
205
|
-
env.KOMO_CONFIG_HOME || join(homedir(), ".config", "komo"),
|
|
206
|
-
`pat-${key}.json`
|
|
207
|
-
);
|
|
215
|
+
return credentialFiles(env, `pat-${key}.json`);
|
|
208
216
|
}
|
|
209
|
-
async function readSaved(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
217
|
+
async function readSaved(paths) {
|
|
218
|
+
for (const path of paths)
|
|
219
|
+
try {
|
|
220
|
+
return JSON.parse(await readFile(path, "utf8")).token || null;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (error.code !== "ENOENT") throw error;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
async function readEach(paths) {
|
|
227
|
+
const tokens = [];
|
|
228
|
+
for (const path of paths) {
|
|
229
|
+
const token = await readSaved([path]);
|
|
230
|
+
if (token) tokens.push(token);
|
|
215
231
|
}
|
|
232
|
+
return tokens;
|
|
216
233
|
}
|
|
217
|
-
// Precedence: KOMO_TOKEN, then a saved access token, then a login session.
|
|
218
|
-
async function readCredentials(
|
|
219
|
-
const saved = { session: await readSaved(
|
|
220
|
-
const
|
|
221
|
-
|
|
234
|
+
// Precedence: PALETTE_TOKEN (or KOMO_TOKEN), then a saved access token, then a login session.
|
|
235
|
+
async function readCredentials(sessionPaths, patPaths, env) {
|
|
236
|
+
const saved = { session: await readSaved(sessionPaths), pat: await readSaved(patPaths) };
|
|
237
|
+
const variable = setting(env, "TOKEN");
|
|
238
|
+
const [source, token] = variable
|
|
239
|
+
? ["env", variable]
|
|
222
240
|
: saved.pat
|
|
223
241
|
? ["token", saved.pat]
|
|
224
242
|
: saved.session
|
|
@@ -238,7 +256,7 @@ async function readTokenFromStdin() {
|
|
|
238
256
|
}
|
|
239
257
|
const token = Buffer.concat(chunks).toString("utf8").trim();
|
|
240
258
|
if (!PERSONAL_TOKEN.test(token))
|
|
241
|
-
throw Error("Expected a Palette access token (
|
|
259
|
+
throw Error("Expected a Palette access token (palette_pat_…) on stdin.");
|
|
242
260
|
return token;
|
|
243
261
|
}
|
|
244
262
|
async function saveToken(path, token) {
|
|
@@ -280,7 +298,7 @@ export function apiClient(config, token) {
|
|
|
280
298
|
if (!response.ok) {
|
|
281
299
|
const error = Error(
|
|
282
300
|
response.status === 401
|
|
283
|
-
? token
|
|
301
|
+
? isPersonalToken(token)
|
|
284
302
|
? "Access token invalid, expired, or revoked. Create a new one at /account."
|
|
285
303
|
: "Session missing or expired. Run palette login."
|
|
286
304
|
: result.error || `Request failed (${response.status}).`
|
|
@@ -371,7 +389,7 @@ async function login(config, path, flags) {
|
|
|
371
389
|
const safe = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
|
|
372
390
|
return send(
|
|
373
391
|
200,
|
|
374
|
-
`<!doctype html><meta charset="utf-8"><title>Sign in to Palette</title><style>body{background:#191919;color:#eee;font:16px system-ui;max-width:420px;margin:18vh auto;padding:24px}button{font:inherit;padding:14px 20px;border:0;border-radius:12px;background:#c8b5f4}p{line-height:1.5;color:#aaa}</style><h1>Connect your agent</h1><p id="project"></p><p>The CLI can read and update comments as you. Your session stays on this computer.</p><button>Continue with Google</button><p id="status"></p><script nonce="${nonce}">document.querySelector('#project').textContent=${safe(config.project)};let popup;document.querySelector('button').onclick=()=>{popup=window.open(${safe(authUrl)},'
|
|
392
|
+
`<!doctype html><meta charset="utf-8"><title>Sign in to Palette</title><style>body{background:#191919;color:#eee;font:16px system-ui;max-width:420px;margin:18vh auto;padding:24px}button{font:inherit;padding:14px 20px;border:0;border-radius:12px;background:#c8b5f4}p{line-height:1.5;color:#aaa}</style><h1>Connect your agent</h1><p id="project"></p><p>The CLI can read and update comments as you. Your session stays on this computer.</p><button>Continue with Google</button><p id="status"></p><script nonce="${nonce}">document.querySelector('#project').textContent=${safe(config.project)};let popup;document.querySelector('button').onclick=()=>{popup=window.open(${safe(authUrl)},'palette-cli-auth','width=500,height=700')};window.addEventListener('message',async e=>{if(e.origin!==${safe(new URL(config.endpoint).origin)}||e.source!==popup||e.data?.type!=='branch-comments:auth')return;const response=await fetch(location.pathname,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:e.data.token})});document.querySelector('#status').textContent=await response.text();if(response.ok){document.querySelector('button').disabled=true;setTimeout(()=>window.close(),400)}});</script>`,
|
|
375
393
|
"text/html"
|
|
376
394
|
);
|
|
377
395
|
}
|
|
@@ -452,7 +470,7 @@ export async function runAgent(
|
|
|
452
470
|
args,
|
|
453
471
|
{ cwd = process.cwd(), env = process.env } = {}
|
|
454
472
|
) {
|
|
455
|
-
if (args.some((arg) =>
|
|
473
|
+
if (args.some((arg) => /(?:palette|komo)_pat_/.test(arg) || arg.startsWith("--token=")))
|
|
456
474
|
throw Error(
|
|
457
475
|
"Never pass a token as an argument: it would appear in shell history and process lists. Pipe it on stdin: printf %s \"$TOKEN\" | palette login --token"
|
|
458
476
|
);
|
|
@@ -484,13 +502,13 @@ export async function runAgent(
|
|
|
484
502
|
{
|
|
485
503
|
version: 1,
|
|
486
504
|
commands: actions,
|
|
487
|
-
auth: "KOMO_TOKEN, palette login --token (stdin), or palette login",
|
|
505
|
+
auth: "PALETTE_TOKEN (or KOMO_TOKEN), palette login --token (stdin), or palette login",
|
|
488
506
|
output: "JSON; prompt is Markdown unless --json",
|
|
489
507
|
designEdit:
|
|
490
|
-
"Design mode messages carry designEdit (Palette DesignEdit v1); prompt prints it as a json
|
|
508
|
+
"Design mode messages carry designEdit (Palette DesignEdit v1); prompt prints it as a json palette-design-edit block",
|
|
491
509
|
snapshots:
|
|
492
|
-
"Threads may carry snapshot { image?, html? } URLs. palette comments snapshots ID (or get/prompt --snapshots) saves them under --out DIR, default .komo/snapshots, and reports the file paths",
|
|
493
|
-
context: ".komo/project.json or --project KEY",
|
|
510
|
+
"Threads may carry snapshot { image?, html? } URLs. palette comments snapshots ID (or get/prompt --snapshots) saves them under --out DIR, default .palette/snapshots (.komo/snapshots in an older project), and reports the file paths",
|
|
511
|
+
context: ".palette/project.json (or .komo/project.json) or --project KEY",
|
|
494
512
|
overrides: ["--endpoint", "--origin", "--repo", "--branch"],
|
|
495
513
|
body: ["--body", "--body-file", "--body-file -"],
|
|
496
514
|
setup: "palette agents setup",
|
|
@@ -518,9 +536,11 @@ export async function runAgent(
|
|
|
518
536
|
if (positional.length > expected)
|
|
519
537
|
throw Error("Unexpected positional arguments. Run palette --help.");
|
|
520
538
|
const config = await configuration(flags, cwd, env),
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const
|
|
539
|
+
paths = credentialPaths(config, env),
|
|
540
|
+
path = paths[0];
|
|
541
|
+
const patPaths = personalTokenPaths(config, env),
|
|
542
|
+
patPath = patPaths[0];
|
|
543
|
+
const { source, token, saved } = await readCredentials(paths, patPaths, env),
|
|
524
544
|
request = apiClient(config, token);
|
|
525
545
|
if (command !== "logout" && config.repo === config.project) {
|
|
526
546
|
const metadata = await request("config");
|
|
@@ -528,8 +548,8 @@ export async function runAgent(
|
|
|
528
548
|
}
|
|
529
549
|
if (flags.snapshots && !(command === "comments" && ["get", "prompt"].includes(action)))
|
|
530
550
|
throw Error("Use --snapshots with palette comments get or prompt.");
|
|
531
|
-
// Downloaded snapshots go under --out, or
|
|
532
|
-
const snapshotDirectory = () => (flags.out ? resolve(cwd, flags.out) : join(config.root ?? cwd, "
|
|
551
|
+
// Downloaded snapshots go under --out, or snapshots/ in the project settings directory.
|
|
552
|
+
const snapshotDirectory = () => (flags.out ? resolve(cwd, flags.out) : join(config.root ?? cwd, config.settingsDir, "snapshots"));
|
|
533
553
|
const snapshotDownload = (thread) => saveSnapshots(config, token, thread, snapshotDirectory(), !flags.out);
|
|
534
554
|
let data;
|
|
535
555
|
if (command === "login" && flags.token) {
|
|
@@ -543,7 +563,7 @@ export async function runAgent(
|
|
|
543
563
|
else {
|
|
544
564
|
if (!token)
|
|
545
565
|
throw Error(
|
|
546
|
-
"Not signed in. Run palette login, pipe a token to palette login --token, or set
|
|
566
|
+
"Not signed in. Run palette login, pipe a token to palette login --token, or set PALETTE_TOKEN."
|
|
547
567
|
);
|
|
548
568
|
if (command === "project")
|
|
549
569
|
data = await runProject(action, flags, request, config, cwd);
|
|
@@ -551,19 +571,23 @@ export async function runAgent(
|
|
|
551
571
|
else if (command === "logout") {
|
|
552
572
|
// Only login sessions are revoked here. Access tokens are revoked on
|
|
553
573
|
// the account page, so logout never destroys a token other jobs share.
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
574
|
+
// Every distinct session goes: the environment's, and those saved under
|
|
575
|
+
// ~/.config/palette and, from before the rename, ~/.config/komo.
|
|
576
|
+
const variable = setting(env, "TOKEN");
|
|
577
|
+
const sessions = new Set([
|
|
578
|
+
...(variable && !isPersonalToken(variable) ? [variable] : []),
|
|
579
|
+
...(await readEach(paths)),
|
|
580
|
+
]);
|
|
581
|
+
for (const session of sessions)
|
|
558
582
|
try {
|
|
559
583
|
await apiClient(config, session)("me", "DELETE");
|
|
560
584
|
} catch (error) {
|
|
561
585
|
if (error.status !== 401 && error.status !== 404) throw error;
|
|
562
586
|
}
|
|
563
|
-
await rm(
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
587
|
+
for (const file of [...paths, ...patPaths]) await rm(file, { force: true });
|
|
588
|
+
for (const name of ["PALETTE_TOKEN", "KOMO_TOKEN"])
|
|
589
|
+
if (env[name])
|
|
590
|
+
console.error(`${name} is still set in this environment; unset it to stop using it.`);
|
|
567
591
|
data = { signedOut: true };
|
|
568
592
|
} else if (["list", "get", "prompt", "snapshots"].includes(action)) {
|
|
569
593
|
let threads = (
|
package/cli/config.mjs
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/** The PALETTE_ setting `name`, or its older KOMO_ name. */
|
|
6
|
+
export const setting = (env, name) => env[`PALETTE_${name}`] || env[`KOMO_${name}`] || undefined;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Where a project keeps its settings and generated client module. New projects
|
|
10
|
+
* use .palette/ and palette.config.js. A project set up before the rename keeps
|
|
11
|
+
* .komo/ and komo.config.js, so its imports and ignored files stay valid.
|
|
12
|
+
*/
|
|
13
|
+
export function projectLayout(root) {
|
|
14
|
+
return !existsSync(join(root, ".palette")) && existsSync(join(root, ".komo"))
|
|
15
|
+
? { dir: ".komo", module: "komo.config.js" }
|
|
16
|
+
: { dir: ".palette", module: "palette.config.js" };
|
|
17
|
+
}
|
|
2
18
|
|
|
3
19
|
export function gitValue(args, cwd) {
|
|
4
20
|
try {
|
|
@@ -19,7 +35,7 @@ export function repository(remote) {
|
|
|
19
35
|
}
|
|
20
36
|
export function branchName(env, cwd) {
|
|
21
37
|
return (
|
|
22
|
-
env
|
|
38
|
+
setting(env, "BRANCH") ||
|
|
23
39
|
env.CF_PAGES_BRANCH ||
|
|
24
40
|
env.WORKERS_CI_BRANCH ||
|
|
25
41
|
env.VERCEL_GIT_COMMIT_REF ||
|
package/cli/design-edit.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// CLI only: reads the structured DesignEdit (v1) out of a Design mode comment.
|
|
2
|
-
// Comments store proposals as text. A fenced `
|
|
2
|
+
// Comments store proposals as text. A fenced `palette-design-edit` (or older
|
|
3
|
+
// `komo-design-edit`) JSON block wins
|
|
3
4
|
// when present and valid; otherwise the edit is derived from the "Proposed …
|
|
4
5
|
// changes (preview only)" sections the browser writes, so old comments work too.
|
|
5
6
|
|
|
@@ -7,7 +8,7 @@
|
|
|
7
8
|
export const DESIGN_EDIT_LIMIT = 16000;
|
|
8
9
|
const BODY_LIMIT = 64000;
|
|
9
10
|
const MAX_CHANGES = 100;
|
|
10
|
-
const FENCE = /```(?:json[ \t]+)?komo-design-edit[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]*```/;
|
|
11
|
+
const FENCE = /```(?:json[ \t]+)?(?:palette|komo)-design-edit[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]*```/;
|
|
11
12
|
const HEADER = /^Proposed (.+?) changes? \(preview only\)$/;
|
|
12
13
|
|
|
13
14
|
const isObject = (value) =>
|
|
@@ -92,13 +93,14 @@ function place(value, to = false) {
|
|
|
92
93
|
|
|
93
94
|
/**
|
|
94
95
|
* A clean copy of `value` if it is a valid DesignEdit v1, else null. Unknown
|
|
95
|
-
* fields are dropped; one invalid change rejects the whole edit.
|
|
96
|
+
* fields are dropped; one invalid change rejects the whole edit. Records from
|
|
97
|
+
* older releases are marked `komo: "design-edit"`; the copy says `palette`.
|
|
96
98
|
*/
|
|
97
99
|
export function validateDesignEdit(value) {
|
|
98
|
-
if (!isObject(value) || value.komo !== "design-edit" || value.version !== 1) return null;
|
|
100
|
+
if (!isObject(value) || (value.palette ?? value.komo) !== "design-edit" || value.version !== 1) return null;
|
|
99
101
|
const target = value.target;
|
|
100
102
|
if (!isObject(target) || !text(target.selector, 2000, false)) return null;
|
|
101
|
-
const edit = {
|
|
103
|
+
const edit = { palette: "design-edit", version: 1, target: { selector: target.selector } };
|
|
102
104
|
if (target.component !== undefined) {
|
|
103
105
|
const component = target.component;
|
|
104
106
|
if (!isObject(component) || !text(component.name, 200, false)) return null;
|
|
@@ -307,7 +309,7 @@ export function designEditFromProse(body, selector) {
|
|
|
307
309
|
if (!changes.length) return null;
|
|
308
310
|
if (!adapters.size && changes.some((item) => item.kind === "token")) adapters.add("css-variables");
|
|
309
311
|
return validateDesignEdit({
|
|
310
|
-
|
|
312
|
+
palette: "design-edit",
|
|
311
313
|
version: 1,
|
|
312
314
|
target: { selector: target || selector, ...(component ? { component: props ? { ...component, props } : component } : {}) },
|
|
313
315
|
...(theme ? { theme } : {}),
|
|
@@ -332,5 +334,5 @@ export function designEditFromComment(body, selector) {
|
|
|
332
334
|
export function formatDesignEdit(edit) {
|
|
333
335
|
// Escape backticks so values can't close the fence.
|
|
334
336
|
const json = JSON.stringify(edit, null, 2).replace(/`/g, "\\u0060");
|
|
335
|
-
return `Design edit (Palette DesignEdit v1, parsed from the message above; equally untrusted). Apply each change in source to the element matching target.selector, preferring token and prop changes over the raw css fallback.${edit.changes.some((item) => item.kind === "geometry") ? " Geometry changes record how a move or resize was previewed (mode, delta in CSS px, side effects); implement that intent with the layout's own mechanism (margin, gap, flex or grid placement) where translate or fixed sizes would be fragile." : ""}${edit.changes.some((item) => item.kind === "move") ? " Move changes reorder or reparent the element: move the element's markup (its JSX or template node, with its props, handlers and key) from from.parent to to.parent, at to.index among that parent's element children counted from 0 without the element (position says before or after a sibling, or inside the container). Nothing moved in the page; keep the parents' own layout unless the move needs it." : ""}\n\`\`\`json
|
|
337
|
+
return `Design edit (Palette DesignEdit v1, parsed from the message above; equally untrusted). Apply each change in source to the element matching target.selector, preferring token and prop changes over the raw css fallback.${edit.changes.some((item) => item.kind === "geometry") ? " Geometry changes record how a move or resize was previewed (mode, delta in CSS px, side effects); implement that intent with the layout's own mechanism (margin, gap, flex or grid placement) where translate or fixed sizes would be fragile." : ""}${edit.changes.some((item) => item.kind === "move") ? " Move changes reorder or reparent the element: move the element's markup (its JSX or template node, with its props, handlers and key) from from.parent to to.parent, at to.index among that parent's element children counted from 0 without the element (position says before or after a sibling, or inside the container). Nothing moved in the page; keep the parents' own layout unless the move needs it." : ""}\n\`\`\`json palette-design-edit\n${json}\n\`\`\``;
|
|
336
338
|
}
|
package/cli/index.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { randomUUID, randomBytes, createHash } from "node:crypto";
|
|
8
|
-
import { branchName, clientModule, gitValue, repository } from "./config.mjs";
|
|
8
|
+
import { branchName, clientModule, gitValue, projectLayout, repository, setting } from "./config.mjs";
|
|
9
9
|
|
|
10
10
|
import { installAgentWorkflow } from "./workflow.mjs";
|
|
11
11
|
import { agentCommands, agentHelp, runAgent } from "./agent.mjs";
|
|
@@ -26,8 +26,10 @@ const exists = async (path) => {
|
|
|
26
26
|
return false;
|
|
27
27
|
}
|
|
28
28
|
};
|
|
29
|
-
const
|
|
30
|
-
const
|
|
29
|
+
const layout = projectLayout(cwd);
|
|
30
|
+
const settingsDir = resolve(cwd, layout.dir);
|
|
31
|
+
const settingsPath = join(settingsDir, "project.json");
|
|
32
|
+
const generatedPath = resolve(cwd, layout.module);
|
|
31
33
|
const run = (program, argv, options = {}) =>
|
|
32
34
|
new Promise((resolveRun, reject) => {
|
|
33
35
|
const child = spawn(program, argv, {
|
|
@@ -67,27 +69,23 @@ async function sync() {
|
|
|
67
69
|
if (config.scope === "branch") {
|
|
68
70
|
config.branch = branchName(process.env, cwd);
|
|
69
71
|
if (!config.branch)
|
|
70
|
-
throw Error("Cannot detect the branch. Set
|
|
72
|
+
throw Error("Cannot detect the branch. Set PALETTE_BRANCH for this build.");
|
|
71
73
|
}
|
|
72
74
|
await writeFile(generatedPath, clientModule(config));
|
|
73
|
-
console.log(
|
|
75
|
+
console.log(`Updated ${layout.module}`);
|
|
74
76
|
}
|
|
75
77
|
async function protectLocalFiles() {
|
|
76
78
|
const path = resolve(cwd, ".gitignore");
|
|
77
79
|
const existing = (await exists(path)) ? await readFile(path, "utf8") : "";
|
|
78
80
|
const additions = [
|
|
79
|
-
"
|
|
80
|
-
|
|
81
|
-
".komo/.dev.vars",
|
|
82
|
-
".komo/node.env",
|
|
83
|
-
".komo/.wrangler/",
|
|
84
|
-
"komo.config.js",
|
|
81
|
+
...["owner-key", "setup.json", ".dev.vars", "node.env", ".wrangler/"].map((file) => `${layout.dir}/${file}`),
|
|
82
|
+
layout.module,
|
|
85
83
|
].filter((line) => !existing.split("\n").includes(line));
|
|
86
84
|
if (additions.length)
|
|
87
85
|
await writeFile(path, `${existing}\n# Palette\n${additions.join("\n")}\n`);
|
|
88
86
|
}
|
|
89
87
|
async function deploy() {
|
|
90
|
-
const dir =
|
|
88
|
+
const dir = settingsDir,
|
|
91
89
|
path = join(dir, "wrangler.json");
|
|
92
90
|
const statePath = join(dir, "deployment.json");
|
|
93
91
|
const state = JSON.parse(await readFile(statePath, "utf8"));
|
|
@@ -139,7 +137,7 @@ async function deploy() {
|
|
|
139
137
|
);
|
|
140
138
|
}
|
|
141
139
|
async function init() {
|
|
142
|
-
if (await exists(
|
|
140
|
+
if (await exists(join(settingsDir, "deployment.json")))
|
|
143
141
|
throw Error("Self-host setup exists. Run palette deploy to resume.");
|
|
144
142
|
const previous = (await exists(settingsPath))
|
|
145
143
|
? JSON.parse(await readFile(settingsPath, "utf8"))
|
|
@@ -150,7 +148,7 @@ async function init() {
|
|
|
150
148
|
((await exists(settingsPath)) || (await exists(generatedPath)))
|
|
151
149
|
)
|
|
152
150
|
throw Error(
|
|
153
|
-
|
|
151
|
+
`Palette is already configured. Edit ${layout.dir}/project.json, then run palette sync.`
|
|
154
152
|
);
|
|
155
153
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
156
154
|
const ask = async (text, fallback = "") => {
|
|
@@ -210,15 +208,19 @@ async function init() {
|
|
|
210
208
|
if (!selfHosted && !nodeHosted) {
|
|
211
209
|
const endpoint =
|
|
212
210
|
flag("--endpoint") ||
|
|
213
|
-
(
|
|
214
|
-
|
|
211
|
+
setting(process.env, "ENDPOINT") ||
|
|
212
|
+
(resuming ? previous.endpoint : "");
|
|
213
|
+
if (!endpoint)
|
|
214
|
+
throw Error(
|
|
215
|
+
"Palette has no default server. Pass --endpoint URL or set PALETTE_ENDPOINT to your Palette server, or self-host with palette init --node (see NODE.md) or palette init --self-host."
|
|
216
|
+
);
|
|
215
217
|
const api = new URL(endpoint);
|
|
216
218
|
if (
|
|
217
219
|
api.protocol !== "https:" &&
|
|
218
220
|
!["localhost", "127.0.0.1"].includes(api.hostname)
|
|
219
221
|
)
|
|
220
222
|
throw Error("The API endpoint must use HTTPS.");
|
|
221
|
-
const setupPath =
|
|
223
|
+
const setupPath = join(settingsDir, "setup.json");
|
|
222
224
|
let setup =
|
|
223
225
|
resuming && (await exists(setupPath))
|
|
224
226
|
? JSON.parse(await readFile(setupPath, "utf8"))
|
|
@@ -228,7 +230,7 @@ async function init() {
|
|
|
228
230
|
setup.expiresAt <= Date.now() ||
|
|
229
231
|
flag("--origin") ||
|
|
230
232
|
flag("--repo") ||
|
|
231
|
-
|
|
233
|
+
endpoint !== previous?.endpoint
|
|
232
234
|
) {
|
|
233
235
|
setup = {
|
|
234
236
|
...(await request(endpoint, "/setup/start", { repo, origins })),
|
|
@@ -245,7 +247,7 @@ async function init() {
|
|
|
245
247
|
await sync();
|
|
246
248
|
await installAgentWorkflow(cwd);
|
|
247
249
|
console.log(
|
|
248
|
-
`\nMount Palette in your app:\n\nimport { initPalette } from '
|
|
250
|
+
`\nMount Palette in your app:\n\nimport { initPalette } from './${layout.module}';\ninitPalette();\n\nOpen ${origin} and choose Connect in the sidebar.\nKeep this terminal open while you start your app in another terminal.\nHosted recovery: ${setup.url}\n`
|
|
249
251
|
);
|
|
250
252
|
console.log("Waiting for your project to connect…");
|
|
251
253
|
const end = setup.expiresAt;
|
|
@@ -284,7 +286,7 @@ async function init() {
|
|
|
284
286
|
const googleClient =
|
|
285
287
|
flag("--google-client-id") || (await ask("Google OAuth client ID"));
|
|
286
288
|
const key = randomBytes(32).toString("hex");
|
|
287
|
-
const dir =
|
|
289
|
+
const dir = settingsDir;
|
|
288
290
|
await mkdir(dir, { recursive: true });
|
|
289
291
|
await protectLocalFiles();
|
|
290
292
|
const projectConfig = {
|
|
@@ -308,7 +310,7 @@ async function init() {
|
|
|
308
310
|
await sync();
|
|
309
311
|
await installAgentWorkflow(cwd);
|
|
310
312
|
console.log(
|
|
311
|
-
`Edit .
|
|
313
|
+
`Edit ${layout.dir}/node.env with your PostgreSQL URL and Google secret.\nDeploy the Node container with those variables.\nRegister ${endpoint}/auth/google/callback in Google Console.\nOpen ${endpoint}/setup?project=${project}#${key} to claim ownership after the API is live.\n`
|
|
312
314
|
);
|
|
313
315
|
return;
|
|
314
316
|
} else {
|
|
@@ -326,7 +328,7 @@ async function init() {
|
|
|
326
328
|
const googleClient =
|
|
327
329
|
flag("--google-client-id") || (await ask("Google OAuth client ID"));
|
|
328
330
|
const key = randomBytes(32).toString("hex");
|
|
329
|
-
const dir =
|
|
331
|
+
const dir = settingsDir;
|
|
330
332
|
await mkdir(dir, { recursive: true });
|
|
331
333
|
await cp(
|
|
332
334
|
join(packageRoot, "server/migrations"),
|
|
@@ -374,7 +376,7 @@ async function init() {
|
|
|
374
376
|
await installAgentWorkflow(cwd);
|
|
375
377
|
console.log("Added the Palette comment workflow to AGENTS.md");
|
|
376
378
|
console.log(
|
|
377
|
-
`\nMount after the page loads:\n\nimport { initPalette } from '@hazem3500/palette';\ninitPalette(${JSON.stringify({ ...config, origin: undefined, ...(config.scope === "branch" ? { branch: branchName(process.env, cwd) } : {}) }, null, 2)});\n\nFor automatic branch detection, use the generated
|
|
379
|
+
`\nMount after the page loads:\n\nimport { initPalette } from '@hazem3500/palette';\ninitPalette(${JSON.stringify({ ...config, origin: undefined, ...(config.scope === "branch" ? { branch: branchName(process.env, cwd) } : {}) }, null, 2)});\n\nFor automatic branch detection, use the generated ${layout.module} helper and run palette sync before your build.\n`
|
|
378
380
|
);
|
|
379
381
|
} finally {
|
|
380
382
|
rl.close();
|
|
@@ -383,7 +385,7 @@ async function init() {
|
|
|
383
385
|
try {
|
|
384
386
|
if (args.includes("--help") || !command)
|
|
385
387
|
console.log(
|
|
386
|
-
`Palette\n\n palette init
|
|
388
|
+
`Palette\n\n palette init --endpoint API_URL Connect to an existing Palette server\n palette init --self-host Deploy your own Worker and D1 database\n palette init --node Prepare a Node/PostgreSQL project\n palette deploy Resume self-hosted deployment\n palette sync Regenerate client settings; detect the current branch\n\nOptions: --origin URL --endpoint API_URL --repo OWNER/REPO --branch-scope\nSelf-host: --google-client-id ID; Node: --preview-origin HTTPS_WILDCARD\n\nComments are shared across deployments unless --branch-scope is set.${
|
|
387
389
|
agentHelp
|
|
388
390
|
}`
|
|
389
391
|
);
|
package/cli/snapshots.mjs
CHANGED
|
@@ -8,9 +8,9 @@ const MAX_BYTES = 512 * 1024;
|
|
|
8
8
|
* Saves a thread's element snapshots under `directory` and returns their paths:
|
|
9
9
|
* THREAD_ID.webp (or .png) for the image and THREAD_ID.html.txt for the HTML,
|
|
10
10
|
* kept as text because it is evidence, never a page to open. Reads use the same
|
|
11
|
-
* credentials as every other command (
|
|
11
|
+
* credentials as every other command (PALETTE_TOKEN, a saved access token or a login
|
|
12
12
|
* session), against the configured endpoint only. Behind a sign-in proxy, that is a
|
|
13
|
-
* session from POST /auth/proxy/session passed as
|
|
13
|
+
* session from POST /auth/proxy/session passed as PALETTE_TOKEN: the proxy refuses
|
|
14
14
|
* access tokens. A
|
|
15
15
|
* default directory ignores itself in Git, since snapshots may hold page content.
|
|
16
16
|
*/
|
|
@@ -40,7 +40,7 @@ export async function saveSnapshots(config, token, thread, directory, ignore = f
|
|
|
40
40
|
response.status === 429
|
|
41
41
|
? "Too many requests. Wait a minute, then run palette comments snapshots THREAD_ID."
|
|
42
42
|
: response.status === 401
|
|
43
|
-
?
|
|
43
|
+
? /^(?:palette|komo)_pat_/.test(token)
|
|
44
44
|
? "Access token invalid, expired, or revoked. Create a new one at /account."
|
|
45
45
|
: "Session missing or expired. Run palette login."
|
|
46
46
|
: reason || `Could not download the ${kind} snapshot (${response.status}).`
|
package/cli/workflow.mjs
CHANGED
|
@@ -2,8 +2,13 @@ import { readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { agentWorkflow } from "../dist/agent-prompt.js";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// The block's markers. A block from before the rename has komo markers; it is
|
|
6
|
+
// found the same way and rewritten with the palette ones.
|
|
7
|
+
const markers = [
|
|
8
|
+
["<!-- palette:workflow:start -->", "<!-- palette:workflow:end -->"],
|
|
9
|
+
["<!-- komo:workflow:start -->", "<!-- komo:workflow:end -->"],
|
|
10
|
+
];
|
|
11
|
+
const [start, end] = markers[0];
|
|
7
12
|
|
|
8
13
|
export async function installAgentWorkflow(cwd) {
|
|
9
14
|
const path = join(cwd, "AGENTS.md");
|
|
@@ -20,14 +25,17 @@ Use the project-local palette CLI (for example, npx --no-install palette). Run p
|
|
|
20
25
|
|
|
21
26
|
${agentWorkflow}
|
|
22
27
|
${end}`;
|
|
23
|
-
const
|
|
24
|
-
const
|
|
28
|
+
const found = markers.filter((pair) => pair.some((marker) => existing.includes(marker)));
|
|
29
|
+
const [from, to] = found[0] ?? markers[0];
|
|
30
|
+
const first = existing.indexOf(from);
|
|
31
|
+
const last = existing.indexOf(to);
|
|
25
32
|
if (
|
|
33
|
+
found.length > 1 ||
|
|
26
34
|
(first === -1) !== (last === -1) ||
|
|
27
35
|
(first !== -1 &&
|
|
28
36
|
(last < first ||
|
|
29
|
-
existing.indexOf(
|
|
30
|
-
existing.indexOf(
|
|
37
|
+
existing.indexOf(from, first + from.length) !== -1 ||
|
|
38
|
+
existing.indexOf(to, last + to.length) !== -1))
|
|
31
39
|
)
|
|
32
40
|
throw Error(
|
|
33
41
|
"AGENTS.md has an incomplete or duplicate Palette workflow block. Repair its markers before retrying."
|
|
@@ -36,7 +44,7 @@ ${end}`;
|
|
|
36
44
|
first === -1
|
|
37
45
|
? `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${existing ? "\n" : ""}${block}
|
|
38
46
|
`
|
|
39
|
-
: existing.slice(0, first) + block + existing.slice(last +
|
|
47
|
+
: existing.slice(0, first) + block + existing.slice(last + to.length);
|
|
40
48
|
if (content !== existing) await writeFile(path, content);
|
|
41
49
|
return { path, changed: content !== existing };
|
|
42
50
|
}
|