@pantheon-systems/p1-next-sdk 0.7.0 → 0.10.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.
Files changed (56) hide show
  1. package/README.md +87 -0
  2. package/bin/lib/cli.js +171 -0
  3. package/bin/lib/detect.js +179 -0
  4. package/bin/lib/fs-ops.js +32 -0
  5. package/bin/lib/git.js +60 -0
  6. package/bin/lib/messages.js +65 -0
  7. package/bin/lib/transform.js +155 -0
  8. package/bin/p1-migrate.js +5 -0
  9. package/dist/P1NextRouterProvider.d.ts +1 -1
  10. package/dist/P1NextRouterProvider.d.ts.map +1 -1
  11. package/dist/auth-handler.d.ts.map +1 -1
  12. package/dist/auth-handler.js +7 -2
  13. package/dist/auth-handler.js.map +1 -1
  14. package/dist/auth-utils.d.ts +2 -0
  15. package/dist/auth-utils.d.ts.map +1 -0
  16. package/dist/auth-utils.js +7 -0
  17. package/dist/auth-utils.js.map +1 -0
  18. package/dist/css-query-fetchers.d.ts +8 -0
  19. package/dist/css-query-fetchers.d.ts.map +1 -0
  20. package/dist/css-query-fetchers.js +48 -0
  21. package/dist/css-query-fetchers.js.map +1 -0
  22. package/dist/editor-paths.d.ts +10 -0
  23. package/dist/editor-paths.d.ts.map +1 -0
  24. package/dist/editor-paths.js +38 -0
  25. package/dist/editor-paths.js.map +1 -0
  26. package/dist/handler.d.ts.map +1 -1
  27. package/dist/handler.js +1 -7
  28. package/dist/handler.js.map +1 -1
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/middleware.d.ts +8 -0
  34. package/dist/middleware.d.ts.map +1 -0
  35. package/dist/middleware.js +46 -0
  36. package/dist/middleware.js.map +1 -0
  37. package/dist/pages-handler.d.ts +44 -14
  38. package/dist/pages-handler.d.ts.map +1 -1
  39. package/dist/pages-handler.js +31 -23
  40. package/dist/pages-handler.js.map +1 -1
  41. package/dist/routes/broker.d.ts.map +1 -1
  42. package/dist/routes/broker.js +79 -9
  43. package/dist/routes/broker.js.map +1 -1
  44. package/dist/routes/datasource-context.d.ts.map +1 -1
  45. package/dist/routes/datasource-context.js +15 -3
  46. package/dist/routes/datasource-context.js.map +1 -1
  47. package/dist/routes/editor-context.d.ts.map +1 -1
  48. package/dist/routes/editor-context.js +39 -2
  49. package/dist/routes/editor-context.js.map +1 -1
  50. package/dist/routes/resolve-preview.js +1 -1
  51. package/dist/routes/resolve-preview.js.map +1 -1
  52. package/dist/server.d.ts +2 -0
  53. package/dist/server.d.ts.map +1 -1
  54. package/dist/server.js +2 -0
  55. package/dist/server.js.map +1 -1
  56. package/package.json +15 -10
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # @pantheon-systems/p1-next-sdk
2
+
3
+ Next.js SDK for the P1 editor. Mounts the editor, its API routes, auth, and published-page
4
+ rendering into a Next.js App Router application with a handful of factories.
5
+
6
+ > Part of Pantheon's **P1** platform. It is published publicly so P1 applications can install
7
+ > it, but it talks to Pantheon-hosted services and is not a general-purpose Next.js library.
8
+ > Pre-1.0: minor versions may carry breaking changes.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @pantheon-systems/p1-next-sdk
14
+ ```
15
+
16
+ Peer dependencies:
17
+
18
+ ```bash
19
+ npm install @puckeditor/core next react react-dom
20
+ ```
21
+
22
+ The fastest way to get a working application is to scaffold one:
23
+
24
+ ```bash
25
+ npm create @pantheon-systems/p1-starter-kit my-app
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ Mount the API routes in `app/p1/api/[...p1]/route.ts`:
31
+
32
+ ```ts
33
+ import { createP1Handler } from "@pantheon-systems/p1-next-sdk/server";
34
+ import config from "../../../../puck.config";
35
+
36
+ const handler = createP1Handler({
37
+ config,
38
+ p1BaseUrl: process.env.NEXT_PUBLIC_CSS_BASE_URL,
39
+ p1ApiKey: process.env.CSS_API_KEY,
40
+ p1SiteId: process.env.NEXT_PUBLIC_CSS_SITE_ID,
41
+ });
42
+
43
+ export const { GET, POST } = handler;
44
+ ```
45
+
46
+ And the editor and dashboard pages:
47
+
48
+ ```ts
49
+ import { createP1Pages } from "@pantheon-systems/p1-next-sdk/server";
50
+ import config from "../../../../puck.config";
51
+
52
+ export const pages = createP1Pages({
53
+ config,
54
+ p1BaseUrl: process.env.NEXT_PUBLIC_CSS_BASE_URL,
55
+ p1ApiKey: process.env.CSS_API_KEY,
56
+ p1SiteId: process.env.NEXT_PUBLIC_CSS_SITE_ID,
57
+ });
58
+ ```
59
+
60
+ Client components render inside `P1NextRouterProvider`, which bridges P1 navigation to the
61
+ Next.js router.
62
+
63
+ ## Entry points
64
+
65
+ | Import | Contents |
66
+ | --- | --- |
67
+ | `@pantheon-systems/p1-next-sdk` | `P1NextRouterProvider` and client-side helpers |
68
+ | `.../server` | `createP1Handler`, `createP1AuthHandler`, `createP1Pages`, `createP1Middleware`, `createCssQueryFetchers` |
69
+
70
+ Transpilation is required, since the P1 packages ship untranspiled ESM:
71
+
72
+ ```js
73
+ // next.config.mjs
74
+ transpilePackages: [
75
+ "@pantheon-systems/css-client",
76
+ "@pantheon-systems/puck-css",
77
+ "@pantheon-systems/p1-next-sdk",
78
+ ],
79
+ ```
80
+
81
+ ## CLI
82
+
83
+ The package ships a `p1-migrate` binary for applying P1 migrations:
84
+
85
+ ```bash
86
+ npx p1-migrate
87
+ ```
package/bin/lib/cli.js ADDED
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Codemod orchestration.
3
+ *
4
+ * `migrate()` is pure-ish and testable (takes a dir, returns a result, throws
5
+ * BailError on unrecognized input). `runCLI()` wraps it with argv parsing and
6
+ * process exit codes for the bin entrypoint.
7
+ *
8
+ * All transforms run before any write, so a bail leaves the tree untouched —
9
+ * the app is never left half-migrated.
10
+ */
11
+
12
+ import { join, relative } from "node:path";
13
+ import { detectApp, assertSuiteVersions, isRouteSpecial } from "./detect.js";
14
+ import { assertCleanTree } from "./git.js";
15
+ import * as fsops from "./fs-ops.js";
16
+ import * as msg from "./messages.js";
17
+ import {
18
+ BailError,
19
+ rewriteEditorClient,
20
+ splitPageFile,
21
+ buildLayoutFile,
22
+ } from "./transform.js";
23
+
24
+ const show = (dir, target) => relative(dir, target) || target;
25
+
26
+ /**
27
+ * The codemod's only irreversible act is removing the old catch-all, so when it
28
+ * holds anything else the bail has to be worth reading: name every file, and
29
+ * separate the ones that just need moving from the ones whose destination is a
30
+ * real decision.
31
+ */
32
+ function extraFilesMessage({ catchAll, extras, p1Dir }, dir) {
33
+ const special = extras.filter(isRouteSpecial);
34
+ const plain = extras.filter((entry) => !isRouteSpecial(entry));
35
+ const group = show(dir, join(p1Dir, "(editor)"));
36
+ const lines = [
37
+ `${show(dir, catchAll)} contains files this codemod does not know how to move:`,
38
+ ];
39
+ if (plain.length > 0) {
40
+ lines.push(
41
+ "",
42
+ ...plain.map((entry) => ` ${entry}`),
43
+ `Move these into ${join(group, "[[...p1]]")}/ and add one ../ to each parent-relative import.`,
44
+ );
45
+ }
46
+ if (special.length > 0) {
47
+ lines.push(
48
+ "",
49
+ ...special.map((entry) => ` ${entry}`),
50
+ "These wrap the route segment. The editor now renders from " +
51
+ `${join(group, "layout.tsx")}, one level up, so they likely belong beside it in ${group}/.`,
52
+ );
53
+ }
54
+ lines.push(
55
+ "",
56
+ "Move them, then re-run — the page split, import depth, and layout are still handled for you.",
57
+ );
58
+ return lines.join("\n");
59
+ }
60
+
61
+ function partialMessage({ catchAll, p1Dir }, dir) {
62
+ return (
63
+ `Both ${show(dir, join(p1Dir, "(editor)"))} and ${show(dir, catchAll)} exist, so an ` +
64
+ "earlier run was interrupted before it removed the old route. Check which files you " +
65
+ `want to keep, delete ${show(dir, catchAll)}, then re-run.`
66
+ );
67
+ }
68
+
69
+ export async function migrate(opts = {}) {
70
+ const dir = opts.dir ?? process.cwd();
71
+ const force = opts.force ?? false;
72
+ const dryRun = opts.dryRun ?? false;
73
+
74
+ const app = detectApp(dir);
75
+ if (app.status === "already-migrated") {
76
+ msg.alreadyMigrated();
77
+ return { changed: false };
78
+ }
79
+ if (app.status === "not-found") {
80
+ throw new BailError(
81
+ `Could not find app/p1/[[...p1]]/page.tsx under ${dir}. Run this from your project root.`,
82
+ );
83
+ }
84
+ if (app.status === "partial") {
85
+ throw new BailError(partialMessage(app, dir));
86
+ }
87
+ if (app.status === "extra-files") {
88
+ throw new BailError(extraFilesMessage(app, dir));
89
+ }
90
+
91
+ // Runs on --dry-run too: a plan the installed suite cannot render is not a
92
+ // plan worth previewing.
93
+ if (assertSuiteVersions(dir).status === "unverified") msg.versionsUnverified();
94
+
95
+ if (!force && !dryRun && assertCleanTree(dir).status === "no-repo") msg.noGitRepo();
96
+
97
+ const { catchAll, p1Dir } = app;
98
+ const editorGroup = join(p1Dir, "(editor)");
99
+ const newCatchAll = join(editorGroup, "[[...p1]]");
100
+
101
+ // Transform everything up front — any bail happens before we touch disk.
102
+ const newEditorClient = rewriteEditorClient(fsops.read(join(catchAll, "editor-client.tsx")));
103
+ const { p1Pages, page } = splitPageFile(fsops.read(join(catchAll, "page.tsx")));
104
+ const layout = buildLayoutFile();
105
+
106
+ const writes = [
107
+ [join(newCatchAll, "editor-client.tsx"), newEditorClient],
108
+ [join(newCatchAll, "p1-pages.tsx"), p1Pages],
109
+ [join(newCatchAll, "page.tsx"), page],
110
+ [join(editorGroup, "layout.tsx"), layout],
111
+ ];
112
+
113
+ for (const [path] of writes) fsops.assertWithin(dir, path);
114
+ fsops.assertWithin(dir, catchAll);
115
+
116
+ if (dryRun) {
117
+ msg.dryRunPlan(writes.map(([path]) => path), catchAll);
118
+ return { changed: false, dryRun: true };
119
+ }
120
+
121
+ fsops.ensureDir(newCatchAll);
122
+ for (const [path, content] of writes) fsops.write(path, content);
123
+ fsops.removeDir(catchAll);
124
+
125
+ msg.success(writes.map(([path]) => path), catchAll);
126
+ return { changed: true };
127
+ }
128
+
129
+ /**
130
+ * Strict on purpose: a silently-ignored `--dryrun` would run the real migration,
131
+ * and a `--dir` given with a space would migrate the current directory instead
132
+ * of the one the user named.
133
+ */
134
+ export function parseArgs(argv) {
135
+ const opts = { dir: process.cwd(), force: false, dryRun: false, help: false };
136
+ for (const arg of argv) {
137
+ if (arg === "--force" || arg === "-f") opts.force = true;
138
+ else if (arg === "--dry-run") opts.dryRun = true;
139
+ else if (arg === "--help" || arg === "-h") opts.help = true;
140
+ else if (arg.startsWith("--dir=")) opts.dir = arg.slice("--dir=".length);
141
+ else {
142
+ throw new BailError(
143
+ `Unrecognized argument: ${arg}. Run with --help to see the supported options.`,
144
+ );
145
+ }
146
+ }
147
+ return opts;
148
+ }
149
+
150
+ export async function runCLI(argv = process.argv.slice(2)) {
151
+ let opts;
152
+ try {
153
+ opts = parseArgs(argv);
154
+ } catch (err) {
155
+ if (err instanceof BailError) msg.bail(err.message);
156
+ else msg.unexpected(err);
157
+ process.exitCode = 1;
158
+ return;
159
+ }
160
+ if (opts.help) {
161
+ msg.help();
162
+ return;
163
+ }
164
+ try {
165
+ await migrate(opts);
166
+ } catch (err) {
167
+ if (err instanceof BailError) msg.bail(err.message);
168
+ else msg.unexpected(err);
169
+ process.exitCode = 1;
170
+ }
171
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Classify a consumer project's editor route so the codemod knows whether to
3
+ * run, skip (already migrated), or bail (unrecognized), and verify the installed
4
+ * package suite is new enough for the shape this codemod writes.
5
+ */
6
+
7
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { BailError } from "./transform.js";
10
+
11
+ /** The only two files the codemod knows how to transform and carry across. */
12
+ const MOVABLE = ["page.tsx", "editor-client.tsx"];
13
+
14
+ /**
15
+ * App Router files whose behavior depends on where they sit. The editor moves
16
+ * up into `(editor)/layout.tsx`, so these stop wrapping it even when relocated
17
+ * faithfully — the destination is a judgment call the codemod should not make.
18
+ */
19
+ const ROUTE_SPECIAL = new Set([
20
+ "layout",
21
+ "template",
22
+ "error",
23
+ "global-error",
24
+ "loading",
25
+ "not-found",
26
+ "default",
27
+ "route",
28
+ ]);
29
+
30
+ export function isRouteSpecial(entry) {
31
+ return ROUTE_SPECIAL.has(entry.replace(/\.(tsx|ts|jsx|js)$/, ""));
32
+ }
33
+
34
+ function extraEntries(catchAll) {
35
+ return readdirSync(catchAll, { withFileTypes: true })
36
+ .filter((entry) => !MOVABLE.includes(entry.name))
37
+ .map((entry) => (entry.isDirectory() ? `${entry.name}/` : entry.name))
38
+ .sort();
39
+ }
40
+
41
+ export function detectApp(dir) {
42
+ const p1Dir = [join(dir, "app", "p1"), join(dir, "src", "app", "p1")].find((p) =>
43
+ existsSync(p),
44
+ );
45
+ if (!p1Dir) return { status: "not-found" };
46
+
47
+ const catchAll = join(p1Dir, "[[...p1]]");
48
+
49
+ if (existsSync(join(p1Dir, "(editor)"))) {
50
+ // Both trees present means a previous run died between the writes and the
51
+ // cleanup; calling that "already migrated" would strand the old route.
52
+ if (existsSync(catchAll)) return { status: "partial", p1Dir, catchAll };
53
+ return { status: "already-migrated", p1Dir };
54
+ }
55
+
56
+ if (
57
+ existsSync(join(catchAll, "page.tsx")) &&
58
+ existsSync(join(catchAll, "editor-client.tsx"))
59
+ ) {
60
+ const extras = extraEntries(catchAll);
61
+ if (extras.length > 0) return { status: "extra-files", p1Dir, catchAll, extras };
62
+ return { status: "legacy", p1Dir, catchAll };
63
+ }
64
+
65
+ return { status: "not-found" };
66
+ }
67
+
68
+ /**
69
+ * The release that moved the editor from `pages.Page` to `pages.Layout`. The
70
+ * codemod writes routes that call `Layout`, so anything older would be
71
+ * restructured to import an export that does not exist yet.
72
+ */
73
+ export const MIN_SUITE_VERSION = "0.8.0";
74
+
75
+ /**
76
+ * The lockstep-versioned packages a consumer app actually installs.
77
+ * `create-p1-starter-kit` is in the same `fixed` group but only ever scaffolds,
78
+ * so it is never present in the tree being migrated.
79
+ */
80
+ const SUITE = [
81
+ "@pantheon-systems/p1-next-sdk",
82
+ "@pantheon-systems/puck-css",
83
+ "@pantheon-systems/css-client",
84
+ ];
85
+
86
+ function readVersion(packageJsonPath) {
87
+ if (!existsSync(packageJsonPath)) return null;
88
+ try {
89
+ return JSON.parse(readFileSync(packageJsonPath, "utf-8")).version ?? null;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ function manifestPath(...segments) {
96
+ return join(...segments, "package.json");
97
+ }
98
+
99
+ /** Numeric release triple, ignoring any prerelease tag. Null when unparseable. */
100
+ function parseVersion(version) {
101
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(String(version));
102
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
103
+ }
104
+
105
+ function isOlderThan(version, floor) {
106
+ for (let i = 0; i < 3; i++) {
107
+ if (version[i] !== floor[i]) return version[i] < floor[i];
108
+ }
109
+ return false;
110
+ }
111
+
112
+ /**
113
+ * Verify the installed suite is consistent and new enough.
114
+ *
115
+ * Reads the installed tree rather than the consumer's declared ranges: a
116
+ * pre-1.0 caret is pinned to its minor, and an exact-pinned internal dep is
117
+ * satisfied by a nested private copy, so ranges cannot reveal either a stale
118
+ * install or a duplicated package. When nothing is resolvable we cannot verify
119
+ * anything — proceed rather than block, matching the clean-tree check.
120
+ *
121
+ * Only root-level packages are checked. Under pnpm's isolated node_modules just
122
+ * the app's direct dependencies are linked at the root, so a suite package
123
+ * missing from there is transitive, not broken — and a genuinely absent one
124
+ * fails loudly at build time anyway.
125
+ */
126
+ export function assertSuiteVersions(dir) {
127
+ const modules = join(dir, "node_modules");
128
+ const resolved = SUITE.map((pkg) => ({
129
+ pkg,
130
+ version: readVersion(manifestPath(modules, pkg)),
131
+ })).filter((entry) => entry.version !== null);
132
+
133
+ if (resolved.length === 0) return { status: "unverified" };
134
+
135
+ const duplicates = [];
136
+ for (const { pkg: owner } of resolved) {
137
+ for (const { pkg: nested, version: root } of resolved) {
138
+ if (owner === nested) continue;
139
+ const version = readVersion(manifestPath(modules, owner, "node_modules", nested));
140
+ if (version !== null && version !== root) {
141
+ duplicates.push(`${nested}@${version} nested under ${owner}, and ${nested}@${root} at the root`);
142
+ }
143
+ }
144
+ }
145
+ if (duplicates.length > 0) {
146
+ throw new BailError(
147
+ `Your install has more than one copy of a P1 package: ${duplicates.join("; ")}. ` +
148
+ "Two copies mean two React contexts and the editor will misbehave at runtime. " +
149
+ "Upgrade every @pantheon-systems/* dependency in your package.json to the same " +
150
+ "version, reinstall, then re-run.",
151
+ );
152
+ }
153
+
154
+ const versions = [...new Set(resolved.map((entry) => entry.version))];
155
+ if (versions.length > 1) {
156
+ throw new BailError(
157
+ "Installed P1 packages are on different versions " +
158
+ `(${resolved.map((e) => `${e.pkg}@${e.version}`).join(", ")}). ` +
159
+ "They are released in lockstep and must match. Upgrade them together, then re-run.",
160
+ );
161
+ }
162
+
163
+ const [version] = versions;
164
+ const parsed = parseVersion(version);
165
+ if (parsed === null) return { status: "unverified" };
166
+
167
+ if (isOlderThan(parsed, parseVersion(MIN_SUITE_VERSION))) {
168
+ throw new BailError(
169
+ `Installed P1 packages are at ${version}, but the persistent (editor) layout ` +
170
+ `needs ${MIN_SUITE_VERSION} or newer. This codemod runs at the latest published ` +
171
+ "version because npx fetches it from the registry, so it can restructure routes " +
172
+ `your installed version cannot render. Note a "^${version}" range will not resolve ` +
173
+ `${MIN_SUITE_VERSION} — pre-1.0 carets are pinned to their minor — so upgrade ` +
174
+ "explicitly, reinstall, then re-run.",
175
+ );
176
+ }
177
+
178
+ return { status: "ok", version };
179
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Filesystem helpers for the codemod, with a path-traversal guard so a write
3
+ * can never land outside the project being migrated.
4
+ */
5
+
6
+ import { mkdirSync, readFileSync, writeFileSync, rmSync } from "node:fs";
7
+ import { resolve, sep } from "node:path";
8
+ import { BailError } from "./transform.js";
9
+
10
+ export function read(path) {
11
+ return readFileSync(path, "utf-8");
12
+ }
13
+
14
+ export function assertWithin(root, target) {
15
+ const r = resolve(root);
16
+ const t = resolve(target);
17
+ if (t !== r && !t.startsWith(r + sep)) {
18
+ throw new BailError(`Refusing to touch a path outside the project: ${target}`);
19
+ }
20
+ }
21
+
22
+ export function ensureDir(path) {
23
+ mkdirSync(path, { recursive: true });
24
+ }
25
+
26
+ export function write(path, content) {
27
+ writeFileSync(path, content);
28
+ }
29
+
30
+ export function removeDir(path) {
31
+ rmSync(path, { recursive: true, force: true });
32
+ }
package/bin/lib/git.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * A file-moving codemod needs a clean tree so the user can review its diff and
3
+ * roll back. Not being a git repo is a state we can report and proceed from;
4
+ * git being present but unable to answer is not — that is an unverified tree
5
+ * wearing the same mask, and the caller would delete files on the strength of
6
+ * a check that never ran.
7
+ */
8
+
9
+ import { execFileSync } from "node:child_process";
10
+ import { BailError } from "./transform.js";
11
+
12
+ function runGit(args, dir) {
13
+ return execFileSync("git", args, {
14
+ cwd: dir,
15
+ encoding: "utf-8",
16
+ stdio: ["ignore", "pipe", "pipe"],
17
+ });
18
+ }
19
+
20
+ function describeFailure(err) {
21
+ const stderr = typeof err?.stderr === "string" ? err.stderr.trim() : "";
22
+ return stderr || (err instanceof Error ? err.message : String(err));
23
+ }
24
+
25
+ function unverified(err) {
26
+ return new BailError(
27
+ `git could not verify the working tree (${describeFailure(err)}). ` +
28
+ "Fix that, or re-run with --force to migrate without a clean-tree check.",
29
+ );
30
+ }
31
+
32
+ /**
33
+ * @returns `{ status: "clean" }` when the target subtree has no pending changes,
34
+ * or `{ status: "no-repo" }` when there is no repository to check. Throws
35
+ * BailError when the tree is dirty or git could not answer.
36
+ */
37
+ export function assertCleanTree(dir, run = runGit) {
38
+ try {
39
+ run(["rev-parse", "--git-dir"], dir);
40
+ } catch (err) {
41
+ if (/not a git repository/i.test(describeFailure(err))) return { status: "no-repo" };
42
+ throw unverified(err);
43
+ }
44
+
45
+ let out;
46
+ try {
47
+ // Scoped to the target: `git status` is repo-wide by default, so an
48
+ // unrelated dirty file elsewhere in a monorepo would block a clean subtree.
49
+ out = run(["status", "--porcelain", "--", "."], dir);
50
+ } catch (err) {
51
+ throw unverified(err);
52
+ }
53
+
54
+ if (out.trim()) {
55
+ throw new BailError(
56
+ "Working tree is not clean. Commit or stash your changes first, or re-run with --force.",
57
+ );
58
+ }
59
+ return { status: "clean" };
60
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * All console output for the codemod, kept in one place. Plain text — no color
3
+ * dependency — so the SDK's runtime deps stay unchanged.
4
+ */
5
+
6
+ import { MIN_SUITE_VERSION } from "./detect.js";
7
+
8
+ const TAG = "[p1-migrate]";
9
+
10
+ export function help() {
11
+ console.log(
12
+ [
13
+ "p1-migrate — migrate a P1 app to the persistent (editor) layout",
14
+ "",
15
+ "Usage: npx @pantheon-systems/p1-next-sdk p1-migrate [options]",
16
+ "",
17
+ "Options:",
18
+ " --dir=<path> Project directory to migrate (default: current directory)",
19
+ " --dry-run Show what would change without writing anything",
20
+ " --force, -f Skip the clean-git-tree check",
21
+ " --help, -h Show this help",
22
+ ].join("\n"),
23
+ );
24
+ }
25
+
26
+ export function alreadyMigrated() {
27
+ console.log(`${TAG} Already on the (editor) layout — nothing to do.`);
28
+ }
29
+
30
+ export function noGitRepo() {
31
+ console.log(
32
+ `${TAG} Not a git repository — skipping the clean-tree check. This rewrites and ` +
33
+ "removes files with no way to roll them back.",
34
+ );
35
+ }
36
+
37
+ export function versionsUnverified() {
38
+ console.log(
39
+ `${TAG} Could not read installed @pantheon-systems/* versions — skipping the version check.`,
40
+ );
41
+ console.log(
42
+ `${TAG} Make sure your dependencies are installed and on ${MIN_SUITE_VERSION} or newer.`,
43
+ );
44
+ }
45
+
46
+ export function dryRunPlan(writePaths, oldDir) {
47
+ console.log(`${TAG} Dry run — no files written. Planned changes:`);
48
+ for (const p of writePaths) console.log(` write ${p}`);
49
+ console.log(` remove ${oldDir}`);
50
+ }
51
+
52
+ export function success(writePaths, oldDir) {
53
+ console.log(`${TAG} Migrated to the persistent (editor) layout:`);
54
+ for (const p of writePaths) console.log(` wrote ${p}`);
55
+ console.log(` removed ${oldDir}`);
56
+ console.log(`${TAG} Review the diff and commit when it looks right.`);
57
+ }
58
+
59
+ export function bail(message) {
60
+ console.error(`${TAG} Could not migrate automatically: ${message}`);
61
+ }
62
+
63
+ export function unexpected(err) {
64
+ console.error(`${TAG} Unexpected error: ${err instanceof Error ? err.message : String(err)}`);
65
+ }