plotlink-ows 1.0.33 → 1.2.95
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 +4 -0
- package/app/lib/active-wallet.ts +260 -0
- package/app/lib/agent-command.ts +85 -0
- package/app/lib/agent-readiness.ts +133 -0
- package/app/lib/apply-schema.ts +55 -0
- package/app/lib/bubble-text.ts +160 -0
- package/app/lib/cartoon-coach.ts +198 -0
- package/app/lib/cartoon-markdown.ts +83 -0
- package/app/lib/cartoon-prompt.ts +122 -0
- package/app/lib/cartoon-readiness.ts +813 -0
- package/app/lib/clean-image-sync.ts +245 -0
- package/app/lib/codex-images.ts +152 -0
- package/app/lib/cut-asset-diagnostics.ts +120 -0
- package/app/lib/cuts.ts +302 -0
- package/app/lib/fonts.ts +109 -0
- package/app/lib/generate-claude-md.ts +8 -1
- package/app/lib/generate-story-instructions.ts +731 -0
- package/app/lib/image-asset-validate.ts +123 -0
- package/app/lib/lettering-status.ts +133 -0
- package/app/lib/overlays.ts +637 -0
- package/app/lib/paths.ts +10 -0
- package/app/lib/public-title.ts +65 -0
- package/app/lib/publish.ts +16 -2
- package/app/lib/story-progress.ts +242 -0
- package/app/lib/terminal-protocol.ts +16 -0
- package/app/lib/terminal-redact.ts +50 -0
- package/app/prisma/schema.sql +25 -0
- package/app/routes/agent.ts +42 -0
- package/app/routes/codex-images.ts +67 -0
- package/app/routes/dashboard.ts +6 -4
- package/app/routes/publish.ts +259 -45
- package/app/routes/settings.ts +92 -37
- package/app/routes/stories.ts +961 -5
- package/app/routes/terminal.ts +383 -31
- package/app/routes/wallet.ts +58 -30
- package/app/server.ts +47 -12
- package/app/vite.config.ts +6 -0
- package/app/web/components/CartoonNextAction.tsx +145 -0
- package/app/web/components/CartoonPreview.tsx +267 -0
- package/app/web/components/CartoonPublishPage.tsx +407 -0
- package/app/web/components/CartoonPublishPreview.tsx +121 -0
- package/app/web/components/CartoonStepGuide.tsx +90 -0
- package/app/web/components/CartoonWorkflowNav.tsx +68 -0
- package/app/web/components/CodexImportPicker.tsx +230 -0
- package/app/web/components/CutListPanel.tsx +1337 -0
- package/app/web/components/Dashboard.tsx +15 -6
- package/app/web/components/EpisodesPage.tsx +80 -0
- package/app/web/components/FinishEpisodePanel.tsx +151 -0
- package/app/web/components/Layout.tsx +7 -4
- package/app/web/components/LetteringEditor.tsx +1182 -0
- package/app/web/components/PreviewPanel.tsx +952 -78
- package/app/web/components/Settings.tsx +63 -0
- package/app/web/components/StoriesPage.tsx +745 -33
- package/app/web/components/StoryBrowser.tsx +22 -14
- package/app/web/components/StoryInfoPage.tsx +266 -0
- package/app/web/components/StoryProgressPanel.tsx +446 -0
- package/app/web/components/TerminalPanel.tsx +233 -11
- package/app/web/components/WalletCard.tsx +110 -8
- package/app/web/components/WorkflowCoach.tsx +156 -0
- package/app/web/components/asset-image.tsx +114 -0
- package/app/web/components/asset-test-utils.ts +44 -0
- package/app/web/components/export-cut.ts +320 -0
- package/app/web/dist/assets/export-cut-che5mMWc.js +1 -0
- package/app/web/dist/assets/index-CcfChGEK.css +32 -0
- package/app/web/dist/assets/index-Dc2TQ3Ij.js +143 -0
- package/app/web/dist/index.html +2 -2
- package/app/web/lib/cartoon-publish-summary.ts +43 -0
- package/app/web/lib/codex-import.ts +94 -0
- package/app/web/lib/image-compress.ts +53 -0
- package/app/web/lib/import-image.ts +58 -0
- package/app/web/lib/publish-helpers.ts +385 -0
- package/app/web/lib/upload-retry.ts +130 -0
- package/app/web/lib/verify-public-title.ts +105 -0
- package/app/web/styles.css +9 -0
- package/bin/plotlink-ows.js +53 -16
- package/bin/startup-plan.cjs +58 -0
- package/lib/genres.ts +92 -0
- package/package.json +60 -20
- package/scripts/gen-schema-sql.mjs +49 -0
- package/scripts/package-hygiene.mjs +116 -0
- package/scripts/preflight.mjs +173 -0
- package/scripts/start-smoke.mjs +128 -0
- package/app/node_modules/.prisma/local-client/client.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/client.js +0 -5
- package/app/node_modules/.prisma/local-client/default.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/default.js +0 -5
- package/app/node_modules/.prisma/local-client/edge.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/edge.js +0 -184
- package/app/node_modules/.prisma/local-client/index-browser.js +0 -173
- package/app/node_modules/.prisma/local-client/index.d.ts +0 -3304
- package/app/node_modules/.prisma/local-client/index.js +0 -207
- package/app/node_modules/.prisma/local-client/libquery_engine-darwin-arm64.dylib.node +0 -0
- package/app/node_modules/.prisma/local-client/package.json +0 -183
- package/app/node_modules/.prisma/local-client/query_engine_bg.js +0 -2
- package/app/node_modules/.prisma/local-client/query_engine_bg.wasm +0 -0
- package/app/node_modules/.prisma/local-client/runtime/edge-esm.js +0 -35
- package/app/node_modules/.prisma/local-client/runtime/edge.js +0 -35
- package/app/node_modules/.prisma/local-client/runtime/index-browser.d.ts +0 -370
- package/app/node_modules/.prisma/local-client/runtime/index-browser.js +0 -17
- package/app/node_modules/.prisma/local-client/runtime/library.d.ts +0 -3982
- package/app/node_modules/.prisma/local-client/runtime/library.js +0 -147
- package/app/node_modules/.prisma/local-client/runtime/react-native.js +0 -84
- package/app/node_modules/.prisma/local-client/runtime/wasm-compiler-edge.js +0 -85
- package/app/node_modules/.prisma/local-client/runtime/wasm-engine-edge.js +0 -38
- package/app/node_modules/.prisma/local-client/schema.prisma +0 -21
- package/app/node_modules/.prisma/local-client/wasm-edge-light-loader.mjs +0 -5
- package/app/node_modules/.prisma/local-client/wasm-worker-loader.mjs +0 -5
- package/app/node_modules/.prisma/local-client/wasm.d.ts +0 -1
- package/app/node_modules/.prisma/local-client/wasm.js +0 -191
- package/app/web/dist/assets/index-B-2Ft7Yv.css +0 -32
- package/app/web/dist/assets/index-DxATSk7X.js +0 -134
- package/packages/cli/node_modules/commander/LICENSE +0 -22
- package/packages/cli/node_modules/commander/Readme.md +0 -1149
- package/packages/cli/node_modules/commander/esm.mjs +0 -16
- package/packages/cli/node_modules/commander/index.js +0 -24
- package/packages/cli/node_modules/commander/lib/argument.js +0 -149
- package/packages/cli/node_modules/commander/lib/command.js +0 -2662
- package/packages/cli/node_modules/commander/lib/error.js +0 -39
- package/packages/cli/node_modules/commander/lib/help.js +0 -709
- package/packages/cli/node_modules/commander/lib/option.js +0 -367
- package/packages/cli/node_modules/commander/lib/suggestSimilar.js +0 -101
- package/packages/cli/node_modules/commander/package-support.json +0 -16
- package/packages/cli/node_modules/commander/package.json +0 -82
- package/packages/cli/node_modules/commander/typings/esm.d.mts +0 -3
- package/packages/cli/node_modules/commander/typings/index.d.ts +0 -1045
- package/packages/cli/node_modules/resolve-from/index.d.ts +0 -31
- package/packages/cli/node_modules/resolve-from/index.js +0 -47
- package/packages/cli/node_modules/resolve-from/license +0 -9
- package/packages/cli/node_modules/resolve-from/package.json +0 -36
- package/packages/cli/node_modules/resolve-from/readme.md +0 -72
- package/packages/cli/node_modules/tsup/LICENSE +0 -21
- package/packages/cli/node_modules/tsup/README.md +0 -75
- package/packages/cli/node_modules/tsup/assets/cjs_shims.js +0 -13
- package/packages/cli/node_modules/tsup/assets/esm_shims.js +0 -9
- package/packages/cli/node_modules/tsup/assets/package.json +0 -3
- package/packages/cli/node_modules/tsup/dist/chunk-DI5BO6XE.js +0 -153
- package/packages/cli/node_modules/tsup/dist/chunk-JZ25TPTY.js +0 -42
- package/packages/cli/node_modules/tsup/dist/chunk-PEEXUWMS.js +0 -6
- package/packages/cli/node_modules/tsup/dist/chunk-TWFEYLU4.js +0 -352
- package/packages/cli/node_modules/tsup/dist/chunk-VGC3FXLU.js +0 -203
- package/packages/cli/node_modules/tsup/dist/cli-default.js +0 -12
- package/packages/cli/node_modules/tsup/dist/cli-main.js +0 -8
- package/packages/cli/node_modules/tsup/dist/cli-node.js +0 -14
- package/packages/cli/node_modules/tsup/dist/index.d.ts +0 -511
- package/packages/cli/node_modules/tsup/dist/index.js +0 -1711
- package/packages/cli/node_modules/tsup/dist/rollup.js +0 -6949
- package/packages/cli/node_modules/tsup/package.json +0 -99
- package/packages/cli/node_modules/tsup/schema.json +0 -362
- package/public/screenshot-1.png +0 -0
- package/public/screenshot-2.png +0 -0
- package/public/screenshot-3.png +0 -0
- package/scripts/e2e-verify.ts +0 -1100
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Regenerate app/prisma/schema.sql from app/prisma/schema.prisma (#484).
|
|
3
|
+
//
|
|
4
|
+
// The installed app applies this committed DDL at startup via the Prisma client
|
|
5
|
+
// (app/lib/apply-schema.ts) instead of running `prisma db push`, so the native
|
|
6
|
+
// Prisma schema-engine is never needed at runtime. Run this (and commit the
|
|
7
|
+
// result) after ANY change to schema.prisma: npm run prisma:sql
|
|
8
|
+
//
|
|
9
|
+
// This uses the schema-engine via `prisma migrate diff` — that's fine here
|
|
10
|
+
// because it runs at DEV/build time on a developer machine, not at user runtime.
|
|
11
|
+
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
13
|
+
import { writeFileSync, readFileSync } from "node:fs";
|
|
14
|
+
import { join, dirname } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { createRequire } from "node:module";
|
|
17
|
+
|
|
18
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
|
+
const schemaPath = join(root, "app", "prisma", "schema.prisma");
|
|
20
|
+
const outPath = join(root, "app", "prisma", "schema.sql");
|
|
21
|
+
|
|
22
|
+
// Resolve the local Prisma CLI (same robust resolution the runtime once used).
|
|
23
|
+
const requireFrom = createRequire(join(root, "__resolver__.js"));
|
|
24
|
+
const prismaPkg = requireFrom.resolve("prisma/package.json");
|
|
25
|
+
const pkg = JSON.parse(readFileSync(prismaPkg, "utf8"));
|
|
26
|
+
const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin.prisma;
|
|
27
|
+
const prismaCli = join(dirname(prismaPkg), binRel);
|
|
28
|
+
|
|
29
|
+
const ddl = execFileSync(
|
|
30
|
+
process.execPath,
|
|
31
|
+
[prismaCli, "migrate", "diff", "--from-empty", "--to-schema-datamodel", schemaPath, "--script"],
|
|
32
|
+
{ encoding: "utf8" },
|
|
33
|
+
).trim();
|
|
34
|
+
|
|
35
|
+
const header = [
|
|
36
|
+
"-- Canonical SQLite DDL for the local writer database.",
|
|
37
|
+
"-- GENERATED from app/prisma/schema.prisma — do not edit by hand.",
|
|
38
|
+
"-- Regenerate after any schema change: npm run prisma:sql",
|
|
39
|
+
"--",
|
|
40
|
+
"-- Applied idempotently at startup via the Prisma client's library query engine",
|
|
41
|
+
"-- (app/lib/apply-schema.ts) so the installed package never invokes the native",
|
|
42
|
+
"-- Prisma schema-engine (`prisma db push`), which fails to spawn in some packed",
|
|
43
|
+
"-- prod-only environments (#484, EPIC #465).",
|
|
44
|
+
"",
|
|
45
|
+
"",
|
|
46
|
+
].join("\n");
|
|
47
|
+
|
|
48
|
+
writeFileSync(outPath, header + ddl + "\n");
|
|
49
|
+
console.log(`Wrote ${outPath}`);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Suspicious-file detection for the release preflight (#466, EPIC #465).
|
|
2
|
+
//
|
|
3
|
+
// Any packed file whose path matches one of these rules must NEVER ship in the
|
|
4
|
+
// published `plotlink-ows` package. The package.json `files` allowlist already
|
|
5
|
+
// excludes them (negation patterns); this is the preflight's belt-and-suspenders
|
|
6
|
+
// detection so a regression is caught before publish. Keep the two in sync.
|
|
7
|
+
|
|
8
|
+
export const SUSPICIOUS_RULES = [
|
|
9
|
+
{ re: /(^|\/)node_modules\//, label: "bundled node_modules" },
|
|
10
|
+
{ re: /\.(test|spec)\.[cm]?[jt]sx?$/, label: "test/spec file" },
|
|
11
|
+
{ re: /(^|\/)(__fixtures__|fixtures)\/|\.fixture\./, label: "test fixture" },
|
|
12
|
+
{ re: /\.snap$/, label: "test snapshot" },
|
|
13
|
+
{ re: /(^|\/)e2e[-/]/, label: "e2e/test tooling" },
|
|
14
|
+
{ re: /\.tgz$/, label: "packed tarball" },
|
|
15
|
+
{ re: /(^|\/)(\.next\/cache|\.turbo|\.vite|\.cache|coverage|\.nyc_output)\//, label: "build/coverage cache" },
|
|
16
|
+
{ re: /(^|\/)screenshots?\/|(^|\/)screenshot-/, label: "screenshot/marketing image" },
|
|
17
|
+
{ re: /(^|\/)(tmp|temp)\/|\.(log|tmp|bak|swp)$/, label: "temp/log file" },
|
|
18
|
+
{ re: /(^|\/)\.env(\..+)?$|\.(pem|key)$/, label: "possible secret/credential file" },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// The runtime files the published package MUST contain. A `files`-allowlist
|
|
22
|
+
// change that drops one of these fails the preflight (#468). `app/web/dist` is
|
|
23
|
+
// required because the CLI serves the prebuilt web UI from it.
|
|
24
|
+
export const REQUIRED_PACK_FILES = [
|
|
25
|
+
"package.json",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"bin/plotlink-ows.js",
|
|
29
|
+
// The bin requires this in-package helper at runtime (start-path boundary
|
|
30
|
+
// planner); `files` ships all of `bin/`, but listing it here fails preflight
|
|
31
|
+
// if a future exclusion drops it (#470).
|
|
32
|
+
"bin/startup-plan.cjs",
|
|
33
|
+
"app/server.ts",
|
|
34
|
+
// Imported by app/server.ts at boot to apply the local SQLite schema without
|
|
35
|
+
// the native Prisma schema-engine (#484).
|
|
36
|
+
"app/lib/apply-schema.ts",
|
|
37
|
+
// The committed DDL apply-schema reads at startup — the installed app applies
|
|
38
|
+
// this instead of running `prisma db push` (#484).
|
|
39
|
+
"app/prisma/schema.sql",
|
|
40
|
+
"app/prisma/schema.prisma",
|
|
41
|
+
"app/web/dist/index.html",
|
|
42
|
+
// Root-lib file the server runtime imports at boot (publish route →
|
|
43
|
+
// `../../lib/genres`); `files` packs only `lib/ows/`, so it must be listed
|
|
44
|
+
// explicitly or the published CLI fails to start (#469).
|
|
45
|
+
"lib/genres.ts",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// The published OWS CLI runtime install path (`dependencies`). EPIC #465 keeps
|
|
49
|
+
// this set MINIMAL — only packages the CLI actually loads at runtime (the server
|
|
50
|
+
// in `app/`, the `bin/` wizard, and the runtime helpers in `lib/`). Web-app
|
|
51
|
+
// (`src/`), build-time, and direct-upload-only packages belong in
|
|
52
|
+
// `devDependencies` (see DEPENDENCIES.md): React/Vite/etc. (#469) and
|
|
53
|
+
// `@aws-sdk/client-s3` (#471 — OWS uploads go through the PlotLink API, so the
|
|
54
|
+
// S3/Filebase client is web-app-only). A new entry here must be a genuine OWS
|
|
55
|
+
// runtime import; add it consciously (and document it) rather than by accident.
|
|
56
|
+
export const ALLOWED_RUNTIME_DEPS = [
|
|
57
|
+
"@hono/node-server",
|
|
58
|
+
"@open-wallet-standard/core",
|
|
59
|
+
"@prisma/client",
|
|
60
|
+
"@supabase/supabase-js",
|
|
61
|
+
"dotenv",
|
|
62
|
+
"hono",
|
|
63
|
+
"node-pty",
|
|
64
|
+
"prisma",
|
|
65
|
+
"tsx",
|
|
66
|
+
"viem",
|
|
67
|
+
"ws",
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Runtime `dependencies` that are NOT in the OWS runtime allowlist — i.e. a
|
|
72
|
+
* web-app/build-time/upload-only package that leaked into the published install
|
|
73
|
+
* path (#471, EPIC #465). An empty array means the install path is clean.
|
|
74
|
+
*/
|
|
75
|
+
export function findRuntimeDepLeaks(pkg) {
|
|
76
|
+
const allowed = new Set(ALLOWED_RUNTIME_DEPS);
|
|
77
|
+
return Object.keys(pkg.dependencies ?? {}).filter((d) => !allowed.has(d));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Return the REQUIRED_PACK_FILES that are NOT in the packed path list. */
|
|
81
|
+
export function findMissingRequired(paths) {
|
|
82
|
+
const set = new Set(paths);
|
|
83
|
+
return REQUIRED_PACK_FILES.filter((req) => !set.has(req));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Return `[{ label, path }]` for every path matching a suspicious rule (first
|
|
88
|
+
* match wins per path). An empty array means the file list is clean.
|
|
89
|
+
*/
|
|
90
|
+
export function findSuspicious(paths) {
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const path of paths) {
|
|
93
|
+
for (const rule of SUSPICIOUS_RULES) {
|
|
94
|
+
if (rule.re.test(path)) { out.push({ label: rule.label, path }); break; }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The files a freshly-installed package MUST contain to function: the bin(s),
|
|
102
|
+
* the app entrypoints, AND every file the install LIFECYCLE references — notably
|
|
103
|
+
* the `--schema <path>` the `postinstall` runs `prisma generate` against (#466,
|
|
104
|
+
* re1). The smoke test asserts these are present, so a `files`-allowlist
|
|
105
|
+
* regression that drops a postinstall prerequisite fails the preflight instead
|
|
106
|
+
* of silently breaking a real `npm install` of the published tarball.
|
|
107
|
+
*/
|
|
108
|
+
export function requiredInstalledFiles(pkg) {
|
|
109
|
+
const required = ["package.json", "app/server.ts", "app/web/dist/index.html"];
|
|
110
|
+
const binPaths = typeof pkg.bin === "string" ? [pkg.bin] : Object.values(pkg.bin ?? {});
|
|
111
|
+
for (const b of binPaths) if (b) required.push(String(b).replace(/^\.?\//, ""));
|
|
112
|
+
// Every `--schema <path>` referenced by the postinstall lifecycle.
|
|
113
|
+
const postinstall = pkg.scripts?.postinstall ?? "";
|
|
114
|
+
for (const m of postinstall.matchAll(/--schema[= ]+(\S+)/g)) required.push(m[1]);
|
|
115
|
+
return [...new Set(required)];
|
|
116
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PlotLink OWS — release preflight & package-hygiene check (#466, EPIC #465).
|
|
3
|
+
//
|
|
4
|
+
// Run before a manual `npm publish` (the operator gate, #472) with:
|
|
5
|
+
//
|
|
6
|
+
// npm run preflight
|
|
7
|
+
//
|
|
8
|
+
// It is READ-ONLY and safe: it never runs `npm audit fix`, never publishes,
|
|
9
|
+
// never needs `npm login`, and never prints secrets/passphrases/wallet data.
|
|
10
|
+
//
|
|
11
|
+
// It reports + checks four things, and exits non-zero on any blocking issue:
|
|
12
|
+
// 1. Expected Node/npm toolchain (from package.json engines / packageManager).
|
|
13
|
+
// 2. A production `npm audit --omit=dev` summary (reported; high/critical warn).
|
|
14
|
+
// 3. The packed-package contents (`npm pack --dry-run`), FAILING on any
|
|
15
|
+
// generated/local artifact that must never ship: bundled node_modules,
|
|
16
|
+
// *.test.* / *.spec.* files, stray *.tgz tarballs, build caches, or obvious
|
|
17
|
+
// secret files (.env/.pem/.key).
|
|
18
|
+
// 4. A packed-tarball install smoke test in a throwaway temp dir (no scripts),
|
|
19
|
+
// verifying the bin + runtime entrypoints land and the bin parses.
|
|
20
|
+
//
|
|
21
|
+
// Keep the SUSPICIOUS rules below in sync with the `files` exclusions in
|
|
22
|
+
// package.json — they are two sides of the same hygiene contract.
|
|
23
|
+
|
|
24
|
+
import { execFileSync } from "node:child_process";
|
|
25
|
+
import { readFileSync, mkdtempSync, rmSync, existsSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { tmpdir } from "node:os";
|
|
27
|
+
import { join, dirname } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { findSuspicious, findMissingRequired, requiredInstalledFiles, findRuntimeDepLeaks } from "./package-hygiene.mjs";
|
|
30
|
+
|
|
31
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
32
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
33
|
+
|
|
34
|
+
const failures = [];
|
|
35
|
+
const warnings = [];
|
|
36
|
+
const section = (t) => console.log(`\n=== ${t} ===`);
|
|
37
|
+
const fail = (m) => { failures.push(m); console.log(` ✗ ${m}`); };
|
|
38
|
+
const warn = (m) => { warnings.push(m); console.log(` ! ${m}`); };
|
|
39
|
+
const ok = (m) => console.log(` ✓ ${m}`);
|
|
40
|
+
|
|
41
|
+
/** Run a command, returning { code, stdout } without throwing (audit/pack exit non-zero on findings). */
|
|
42
|
+
function run(cmd, args, opts = {}) {
|
|
43
|
+
try {
|
|
44
|
+
const stdout = execFileSync(cmd, args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 64 * 1024 * 1024, ...opts });
|
|
45
|
+
return { code: 0, stdout };
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return { code: e.status ?? 1, stdout: e.stdout?.toString() ?? "" };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// 1) Expected toolchain
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
section("Expected toolchain");
|
|
55
|
+
console.log(` engines.node: ${pkg.engines?.node ?? "(unset)"}`);
|
|
56
|
+
console.log(` engines.npm: ${pkg.engines?.npm ?? "(unset)"}`);
|
|
57
|
+
console.log(` packageManager: ${pkg.packageManager ?? "(unset)"}`);
|
|
58
|
+
const haveNode = process.version;
|
|
59
|
+
const haveNpm = run("npm", ["-v"]).stdout.trim() || "?";
|
|
60
|
+
console.log(` running node ${haveNode} / npm ${haveNpm}`);
|
|
61
|
+
if (!/^v20\./.test(haveNode)) warn(`Node ${haveNode} is not 20.x — publish should run on Node 20.x (engines.node).`);
|
|
62
|
+
if (!/^10\./.test(haveNpm)) warn(`npm ${haveNpm} is not 10.x — publish should run on npm 10.x (engines.npm / packageManager).`);
|
|
63
|
+
if (warnings.length === 0) ok("running on the expected Node 20.x / npm 10.x toolchain");
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// 2) Production audit summary (reported; never auto-fixed)
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
section("Production audit (npm audit --omit=dev)");
|
|
69
|
+
const audit = run("npm", ["audit", "--omit=dev", "--json"]);
|
|
70
|
+
try {
|
|
71
|
+
const vulns = JSON.parse(audit.stdout).metadata?.vulnerabilities ?? {};
|
|
72
|
+
console.log(` ${JSON.stringify(vulns)}`);
|
|
73
|
+
const severe = (vulns.high ?? 0) + (vulns.critical ?? 0);
|
|
74
|
+
if (severe > 0) warn(`${severe} high/critical production vulnerabilit${severe === 1 ? "y" : "ies"} — review before publishing (do NOT run 'npm audit fix --force').`);
|
|
75
|
+
else ok("no high/critical production vulnerabilities");
|
|
76
|
+
} catch {
|
|
77
|
+
warn("could not parse `npm audit` output (offline, or registry unreachable) — re-run with network access before publishing.");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// 2b) Runtime dependency boundary (#471, EPIC #465)
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
section("Runtime dependency boundary (dependencies allowlist)");
|
|
84
|
+
const leaks = findRuntimeDepLeaks(pkg);
|
|
85
|
+
if (leaks.length) {
|
|
86
|
+
fail(`web-app/build/upload-only package(s) in runtime 'dependencies' (move to devDependencies, or add to ALLOWED_RUNTIME_DEPS if genuinely a runtime import): ${leaks.join(", ")}`);
|
|
87
|
+
} else {
|
|
88
|
+
ok(`runtime 'dependencies' match the OWS allowlist (${Object.keys(pkg.dependencies ?? {}).length} pkgs; no web-app/S3/build leaks)`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// 3) Packed contents + suspicious-file detection
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
section("Packed package contents (npm pack --dry-run)");
|
|
95
|
+
const dry = run("npm", ["pack", "--dry-run", "--json"]);
|
|
96
|
+
let manifest = null;
|
|
97
|
+
try { manifest = JSON.parse(dry.stdout)[0]; } catch { fail("`npm pack --dry-run --json` did not return a parseable manifest."); }
|
|
98
|
+
if (manifest) {
|
|
99
|
+
console.log(` entries: ${manifest.entryCount} unpacked: ${(manifest.unpackedSize / 1024).toFixed(0)}KB tarball: ${(manifest.size / 1024).toFixed(0)}KB`);
|
|
100
|
+
const paths = manifest.files.map((f) => f.path);
|
|
101
|
+
const bad = findSuspicious(paths);
|
|
102
|
+
if (bad.length) {
|
|
103
|
+
fail(`${bad.length} suspicious file(s) in the packed package (fix the package.json 'files' exclusions):`);
|
|
104
|
+
bad.slice(0, 40).forEach((b) => console.log(` - ${b.label}: ${b.path}`));
|
|
105
|
+
if (bad.length > 40) console.log(` … and ${bad.length - 40} more`);
|
|
106
|
+
} else {
|
|
107
|
+
ok("clean: no node_modules / test / fixture / coverage / screenshot / tarball / cache / temp / secret files");
|
|
108
|
+
}
|
|
109
|
+
// Enforce that the required runtime contents are still present (#468) — a
|
|
110
|
+
// `files` exclusion that's too aggressive (dropping the bin, README, LICENSE,
|
|
111
|
+
// the Prisma schema, or the prebuilt web UI) fails here.
|
|
112
|
+
const missing = findMissingRequired(paths);
|
|
113
|
+
if (missing.length) fail(`packed package is missing required runtime file(s): ${missing.join(", ")}`);
|
|
114
|
+
else ok("required runtime contents present (bin, README, LICENSE, server, Prisma schema, web dist)");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// 4) Packed-tarball install smoke test (temp dir, no install scripts)
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
section("Tarball install smoke test");
|
|
121
|
+
let tmp;
|
|
122
|
+
try {
|
|
123
|
+
tmp = mkdtempSync(join(tmpdir(), "plotlink-preflight-"));
|
|
124
|
+
// Pack the real tarball INTO the temp dir, so no stray .tgz is left in the repo.
|
|
125
|
+
const packed = run("npm", ["pack", "--pack-destination", tmp, "--json"]);
|
|
126
|
+
const tgz = JSON.parse(packed.stdout)[0]?.filename;
|
|
127
|
+
if (!tgz) throw new Error("npm pack did not report a tarball filename");
|
|
128
|
+
const tgzPath = join(tmp, tgz);
|
|
129
|
+
// A throwaway consumer project that installs the tarball.
|
|
130
|
+
writeFileSync(join(tmp, "package.json"), JSON.stringify({ name: "preflight-smoke", private: true, version: "0.0.0" }) + "\n");
|
|
131
|
+
// --ignore-scripts: don't run the package's postinstall (prisma generate) — this
|
|
132
|
+
// is a packaging smoke test, not a full runtime bring-up (that's the operator gate).
|
|
133
|
+
const install = run("npm", ["install", tgzPath, "--ignore-scripts", "--no-audit", "--no-fund", "--no-save"], { cwd: tmp });
|
|
134
|
+
if (install.code !== 0) throw new Error("`npm install <tarball>` failed in the temp project");
|
|
135
|
+
const installed = join(tmp, "node_modules", pkg.name);
|
|
136
|
+
// Includes the postinstall prerequisite (the Prisma schema) derived from the
|
|
137
|
+
// actual postinstall command, so dropping it from `files` fails here even
|
|
138
|
+
// though the install ran with --ignore-scripts (#466, re1).
|
|
139
|
+
const required = requiredInstalledFiles(pkg);
|
|
140
|
+
const missing = required.filter((f) => !existsSync(join(installed, f)));
|
|
141
|
+
if (missing.length) fail(`installed tarball is missing required runtime/postinstall file(s): ${missing.join(", ")}`);
|
|
142
|
+
else ok(`tarball installs cleanly; bin + runtime + postinstall prerequisites present (${required.length} checked)`);
|
|
143
|
+
const check = run(process.execPath, ["--check", join(installed, "bin/plotlink-ows.js")]);
|
|
144
|
+
if (check.code !== 0) fail("bin/plotlink-ows.js failed `node --check` (syntax error)");
|
|
145
|
+
else ok("bin/plotlink-ows.js passes node --check");
|
|
146
|
+
} catch (e) {
|
|
147
|
+
fail(`tarball install smoke test failed: ${e.message || e}`);
|
|
148
|
+
} finally {
|
|
149
|
+
if (tmp) rmSync(tmp, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// 5) Prod-only START smoke — packed tarball install + real server boot (#479)
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
section("Prod-only start smoke (packed tarball install + boot)");
|
|
156
|
+
if (process.env.PREFLIGHT_SKIP_START_SMOKE === "1") {
|
|
157
|
+
warn("start smoke SKIPPED via PREFLIGHT_SKIP_START_SMOKE=1 — a skipped run is NOT publish-safe; re-run without it before publishing.");
|
|
158
|
+
} else {
|
|
159
|
+
const startSmoke = run(process.execPath, [join(root, "scripts", "start-smoke.mjs")], { stdio: ["ignore", "inherit", "inherit"] });
|
|
160
|
+
if (startSmoke.code !== 0) fail("prod-only start smoke failed — the packed tarball did not install+boot+serve (see output above).");
|
|
161
|
+
else ok("packed tarball installs prod-only and the server serves /api/auth/status + / (the prebuilt web UI)");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Summary
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
section("Preflight summary");
|
|
168
|
+
console.log(` warnings: ${warnings.length} failures: ${failures.length}`);
|
|
169
|
+
if (failures.length) {
|
|
170
|
+
console.log(`\n✗ Preflight FAILED — ${failures.length} blocking issue(s). Do not publish.`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
console.log(`\n✓ Preflight passed${warnings.length ? ` (with ${warnings.length} warning(s) to review)` : ""}.`);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Prod-only packed-tarball START smoke (#479, EPIC #465).
|
|
3
|
+
//
|
|
4
|
+
// The pack/install smoke in preflight only checks FILE presence with
|
|
5
|
+
// `--ignore-scripts`; it cannot catch a *startup* regression. This smoke does a
|
|
6
|
+
// real bring-up:
|
|
7
|
+
// 1. `npm pack` the real tarball.
|
|
8
|
+
// 2. `npm install --omit=dev` it (scripts ON — postinstall `prisma generate`
|
|
9
|
+
// and native builds run, exactly like a user install).
|
|
10
|
+
// 3. Assert the web-app/build deps removed in #469/#471 are ABSENT.
|
|
11
|
+
// 4. Start the CLI via its real bin with a FRESH HOME + minimal config, then
|
|
12
|
+
// assert the HTTP server actually serves `/api/auth/status` and `/`.
|
|
13
|
+
//
|
|
14
|
+
// This is the check that catches the #479 failure (the server exiting during
|
|
15
|
+
// `prisma db push` in a packed prod-only install). Exits non-zero on any
|
|
16
|
+
// failure and prints the captured server output so a real failure is diagnosable.
|
|
17
|
+
//
|
|
18
|
+
// Heavier than the pack smoke (installs from the registry + boots the server +
|
|
19
|
+
// builds native deps), so it is a publish-gate check run by preflight. Set
|
|
20
|
+
// PREFLIGHT_SKIP_START_SMOKE=1 to skip during local iteration (preflight warns
|
|
21
|
+
// loudly when skipped — a skipped run is NOT publish-safe).
|
|
22
|
+
|
|
23
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
24
|
+
import { mkdtempSync, rmSync, existsSync, writeFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
25
|
+
import { tmpdir } from "node:os";
|
|
26
|
+
import { join, dirname } from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
|
|
29
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
30
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
31
|
+
const PORT = Number(process.env.SMOKE_PORT) || 7787;
|
|
32
|
+
const REMOVED_DEPS = ["@aws-sdk/client-s3", "react", "vite"]; // moved to devDeps in #469/#471
|
|
33
|
+
const BOOT_TIMEOUT_MS = 60_000;
|
|
34
|
+
|
|
35
|
+
let failures = 0;
|
|
36
|
+
const ok = (m) => console.log(` ✓ ${m}`);
|
|
37
|
+
const fail = (m) => { failures++; console.log(` ✗ ${m}`); };
|
|
38
|
+
const sh = (cmd, args, opts = {}) => execFileSync(cmd, args, { encoding: "utf8", ...opts });
|
|
39
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
40
|
+
|
|
41
|
+
const tmp = mkdtempSync(join(tmpdir(), "plotlink-start-smoke-"));
|
|
42
|
+
const home = join(tmp, "home");
|
|
43
|
+
mkdirSync(home, { recursive: true });
|
|
44
|
+
let child;
|
|
45
|
+
let serverOut = "";
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// 1) Pack the real tarball into the temp dir (no stray .tgz in the repo).
|
|
49
|
+
const tgz = JSON.parse(sh("npm", ["pack", "--pack-destination", tmp, "--json"], { cwd: root }))[0].filename;
|
|
50
|
+
|
|
51
|
+
// 2) Throwaway consumer; install prod-only WITH scripts (real user install).
|
|
52
|
+
writeFileSync(join(tmp, "package.json"), JSON.stringify({ name: "start-smoke", private: true, version: "0.0.0" }) + "\n");
|
|
53
|
+
sh("npm", ["install", join(tmp, tgz), "--omit=dev", "--no-audit", "--no-fund", "--no-save"], { cwd: tmp, stdio: "inherit" });
|
|
54
|
+
const installed = join(tmp, "node_modules", pkg.name);
|
|
55
|
+
if (!existsSync(installed)) throw new Error("package did not install into the consumer project");
|
|
56
|
+
ok("tarball installed prod-only (--omit=dev, scripts on)");
|
|
57
|
+
|
|
58
|
+
// 3) The web-app/build deps removed in #469/#471 must be absent (hoisted or nested).
|
|
59
|
+
const leaked = REMOVED_DEPS.filter(
|
|
60
|
+
(d) => existsSync(join(tmp, "node_modules", d)) || existsSync(join(installed, "node_modules", d)),
|
|
61
|
+
);
|
|
62
|
+
if (leaked.length) fail(`removed web-app/build deps present in prod install: ${leaked.join(", ")}`);
|
|
63
|
+
else ok(`removed deps absent from prod install (${REMOVED_DEPS.join(", ")})`);
|
|
64
|
+
|
|
65
|
+
// 4) Fresh HOME + minimal config so the bin starts the server (skips the wizard).
|
|
66
|
+
const cfgDir = join(home, ".plotlink-ows");
|
|
67
|
+
mkdirSync(cfgDir, { recursive: true });
|
|
68
|
+
writeFileSync(
|
|
69
|
+
join(cfgDir, "config.json"),
|
|
70
|
+
JSON.stringify({ port: PORT, passphrase_hash: "smoke", wallet_name: "plotlink-writer", created_at: "2026-01-01T00:00:00Z" }) + "\n",
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// 5) Start via the real bin (exercises cmdStart → server → prisma db push).
|
|
74
|
+
const binPath = join(installed, "bin", "plotlink-ows.js");
|
|
75
|
+
child = spawn(process.execPath, [binPath], {
|
|
76
|
+
cwd: installed,
|
|
77
|
+
// PLOTLINK_OWS_NO_OPEN=1 stops the bin auto-opening a browser during this
|
|
78
|
+
// non-interactive release check (#481); normal `npx plotlink-ows` is unaffected.
|
|
79
|
+
env: { ...process.env, HOME: home, USERPROFILE: home, APP_PORT: String(PORT), PLOTLINK_OWS_NO_OPEN: "1" },
|
|
80
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
81
|
+
detached: true, // own process group, so we can kill the bin AND its server child
|
|
82
|
+
});
|
|
83
|
+
child.stdout.on("data", (d) => (serverOut += d));
|
|
84
|
+
child.stderr.on("data", (d) => (serverOut += d));
|
|
85
|
+
|
|
86
|
+
const deadline = Date.now() + BOOT_TIMEOUT_MS;
|
|
87
|
+
let statusOk = false;
|
|
88
|
+
let rootOk = false;
|
|
89
|
+
while (Date.now() < deadline) {
|
|
90
|
+
if (child.exitCode !== null) break; // bin exited (e.g. db push failed)
|
|
91
|
+
try {
|
|
92
|
+
const r = await fetch(`http://localhost:${PORT}/api/auth/status`);
|
|
93
|
+
if (r.ok) {
|
|
94
|
+
statusOk = true;
|
|
95
|
+
const rootRes = await fetch(`http://localhost:${PORT}/`);
|
|
96
|
+
rootOk = rootRes.ok && /<!doctype html|<html/i.test(await rootRes.text());
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
/* not listening yet */
|
|
101
|
+
}
|
|
102
|
+
await sleep(1000);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (statusOk) ok("server serves GET /api/auth/status");
|
|
106
|
+
else fail(`server did not serve /api/auth/status within ${BOOT_TIMEOUT_MS / 1000}s (bin exitCode=${child.exitCode})`);
|
|
107
|
+
if (rootOk) ok("server serves GET / (prebuilt web UI)");
|
|
108
|
+
else fail("server did not serve the prebuilt web UI at /");
|
|
109
|
+
|
|
110
|
+
if (failures > 0 && serverOut.trim()) {
|
|
111
|
+
console.log("\n --- captured server output ---");
|
|
112
|
+
for (const line of serverOut.trimEnd().split("\n")) console.log(` | ${line}`);
|
|
113
|
+
}
|
|
114
|
+
} catch (e) {
|
|
115
|
+
fail(`start smoke errored: ${e?.message || e}`);
|
|
116
|
+
if (serverOut.trim()) {
|
|
117
|
+
console.log("\n --- captured server output ---");
|
|
118
|
+
for (const line of serverOut.trimEnd().split("\n")) console.log(` | ${line}`);
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
if (child && child.pid && child.exitCode === null) {
|
|
122
|
+
try { process.kill(-child.pid, "SIGTERM"); } catch { /* group gone */ }
|
|
123
|
+
try { child.kill("SIGKILL"); } catch { /* already dead */ }
|
|
124
|
+
}
|
|
125
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
process.exit(failures > 0 ? 1 : 0);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./index"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./index"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./default"
|