ninepm 0.0.1 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ import type { FileManifestEntry } from "./cli-types.js";
2
+ export declare function buildFileManifest(rootDirectory: string): Promise<FileManifestEntry[]>;
3
+ export declare function readManifestFile(rootDirectory: string, manifestPath: string): Promise<Uint8Array>;
4
+ export declare function validateManifest(files: FileManifestEntry[]): FileManifestEntry[];
@@ -0,0 +1,134 @@
1
+ // AUTO-GENERATED by scripts/generate-cli-package.mjs from the in-repo CLI source -- do not edit.
2
+ // Edit src/cli or src/shared instead; this is regenerated on typecheck/test/build/pack.
3
+ import { createHash } from "node:crypto";
4
+ import { lstat, readdir, readFile } from "node:fs/promises";
5
+ import path from "node:path";
6
+ const DEFAULT_IGNORES = new Set([
7
+ ".git",
8
+ "node_modules",
9
+ ".wrangler",
10
+ "dist",
11
+ "dist-test",
12
+ ]);
13
+ // Local-secret files hold API keys, DB creds, and tokens and must never be
14
+ // uploaded as deployable assets. Covers dotenv (`.env`, `.env.*`) and Wrangler
15
+ // dev vars (`.dev.vars`, `.dev.vars.*`), while keeping committed templates
16
+ // (`.env.example`, `.env.sample`, `.env.template`).
17
+ function isLocalSecretFile(name) {
18
+ return ((/^\.env(\.|$)/.test(name) || /^\.dev\.vars(\.|$)/.test(name)) &&
19
+ !/\.(example|sample|template)$/.test(name));
20
+ }
21
+ function isIgnoredEntry(name) {
22
+ return DEFAULT_IGNORES.has(name) || isLocalSecretFile(name);
23
+ }
24
+ export async function buildFileManifest(rootDirectory) {
25
+ const root = path.resolve(rootDirectory);
26
+ // Reject a symlinked deploy root: walkFiles' readdir would follow it and
27
+ // publish the target's files under normal relative paths, so the per-entry
28
+ // symlink skip below is not enough on its own. A link like `dist ->
29
+ // ../../secrets` must fail loudly rather than silently exfiltrate its target.
30
+ if ((await lstat(root)).isSymbolicLink()) {
31
+ throw new Error(`Deploy path is a symlink: ${rootDirectory}. Pass the real target directory instead.`);
32
+ }
33
+ const files = await walkFiles(root);
34
+ const manifest = [];
35
+ for (const absolutePath of files) {
36
+ const bytes = await readFile(absolutePath);
37
+ const relativePath = normalizeManifestPath(path.relative(root, absolutePath));
38
+ manifest.push({
39
+ path: relativePath,
40
+ size: bytes.byteLength,
41
+ sha256: createHash("sha256").update(bytes).digest("hex"),
42
+ contentType: contentTypeForPath(relativePath),
43
+ });
44
+ }
45
+ return manifest.sort((left, right) => left.path.localeCompare(right.path));
46
+ }
47
+ export async function readManifestFile(rootDirectory, manifestPath) {
48
+ const root = path.resolve(rootDirectory);
49
+ const absolutePath = path.resolve(root, manifestPath);
50
+ if (!absolutePath.startsWith(root + path.sep) && absolutePath !== root) {
51
+ throw new Error(`Manifest path escapes root directory: ${manifestPath}`);
52
+ }
53
+ return readFile(absolutePath);
54
+ }
55
+ export function validateManifest(files) {
56
+ const seen = new Set();
57
+ return files.map((file) => {
58
+ if (!file.path || file.path.startsWith("/") || file.path.includes("..")) {
59
+ throw new Error(`Invalid manifest path: ${file.path}`);
60
+ }
61
+ if (seen.has(file.path)) {
62
+ throw new Error(`Duplicate manifest path: ${file.path}`);
63
+ }
64
+ seen.add(file.path);
65
+ if (!Number.isInteger(file.size) || file.size < 0) {
66
+ throw new Error(`Invalid size for ${file.path}`);
67
+ }
68
+ if (!/^[a-f0-9]{64}$/.test(file.sha256)) {
69
+ throw new Error(`Invalid sha256 for ${file.path}`);
70
+ }
71
+ return {
72
+ path: file.path,
73
+ size: file.size,
74
+ sha256: file.sha256,
75
+ contentType: file.contentType || contentTypeForPath(file.path),
76
+ };
77
+ });
78
+ }
79
+ async function walkFiles(directory) {
80
+ const entries = await readdir(directory);
81
+ const files = [];
82
+ for (const entry of entries) {
83
+ if (isIgnoredEntry(entry)) {
84
+ continue;
85
+ }
86
+ const absolutePath = path.join(directory, entry);
87
+ // lstat, not stat: never follow symlinks. A symlink inside the deploy
88
+ // directory (e.g. `config -> ../../.env`, `key -> ~/.ssh/id_ed25519`) would
89
+ // otherwise be read and published as a public asset, escaping both the root
90
+ // and the local-secret name filter. Skipping symlinks also avoids cyclic
91
+ // recursion.
92
+ const details = await lstat(absolutePath);
93
+ if (details.isSymbolicLink()) {
94
+ continue;
95
+ }
96
+ if (details.isDirectory()) {
97
+ files.push(...(await walkFiles(absolutePath)));
98
+ continue;
99
+ }
100
+ if (details.isFile()) {
101
+ files.push(absolutePath);
102
+ }
103
+ }
104
+ return files;
105
+ }
106
+ function normalizeManifestPath(value) {
107
+ return value.split(path.sep).join("/");
108
+ }
109
+ function contentTypeForPath(filePath) {
110
+ const extension = path.extname(filePath).toLowerCase();
111
+ switch (extension) {
112
+ case ".html":
113
+ return "text/html; charset=utf-8";
114
+ case ".css":
115
+ return "text/css; charset=utf-8";
116
+ case ".js":
117
+ case ".mjs":
118
+ case ".ts":
119
+ return "text/javascript; charset=utf-8";
120
+ case ".json":
121
+ return "application/json; charset=utf-8";
122
+ case ".svg":
123
+ return "image/svg+xml";
124
+ case ".png":
125
+ return "image/png";
126
+ case ".jpg":
127
+ case ".jpeg":
128
+ return "image/jpeg";
129
+ case ".webp":
130
+ return "image/webp";
131
+ default:
132
+ return "application/octet-stream";
133
+ }
134
+ }
package/package.json CHANGED
@@ -1,12 +1,39 @@
1
1
  {
2
2
  "name": "ninepm",
3
- "version": "0.0.1",
4
- "description": "Placeholder for the 9pm.ai deploy CLI. The real CLI ships under this name soon.",
5
- "homepage": "https://9pm.ai",
3
+ "version": "0.24.1",
4
+ "description": "CLI for deploying built apps to 9pm.ai.",
5
+ "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
7
- "keywords": [
8
- "9pm",
9
- "deploy",
10
- "cli"
11
- ]
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "bin": {
11
+ "9pm": "./dist/index.js"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md",
22
+ "LICENSE.md"
23
+ ],
24
+ "scripts": {
25
+ "generate": "node ../../scripts/generate-cli-package.mjs",
26
+ "prebuild": "node ../../scripts/generate-cli-package.mjs",
27
+ "build": "tsc -p tsconfig.json",
28
+ "pretypecheck": "node ../../scripts/generate-cli-package.mjs",
29
+ "typecheck": "tsc --noEmit -p tsconfig.json",
30
+ "prepack": "npm run build"
31
+ },
32
+ "engines": {
33
+ "node": ">=20"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^24.10.1",
37
+ "typescript": "^5.9.3"
38
+ }
12
39
  }