niral 0.1.1 → 0.1.3

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 ADDED
@@ -0,0 +1,90 @@
1
+ # Niral CLI
2
+
3
+ Zero-dependency launcher for the Niral full-stack framework. Requires Node.js 22+.
4
+
5
+ ## Use
6
+
7
+ ```sh
8
+ npx niral create myapp
9
+ cd myapp
10
+ npx niral dev
11
+ ```
12
+
13
+ Or install one global launcher:
14
+
15
+ ```sh
16
+ npm install -g niral
17
+ niral create myapp --template blog
18
+ niral dev myapp
19
+ ```
20
+
21
+ Both `niral` and `create-niral` packages provide the same two executable names.
22
+ Install only one globally to avoid bin conflicts. Templates: `minimal` (default),
23
+ `blog`, `dashboard`. Create refuses to overwrite a nonempty project directory.
24
+
25
+ The first project command downloads the framework from the official
26
+ [repository](https://github.com/Zyora-Dev/niral) into `~/.niral/framework`.
27
+ Set `NIRAL_HOME` to choose another install directory. Normal commands reuse the
28
+ cached framework without checking the network. Keep projects outside that directory.
29
+
30
+ ## Commands
31
+
32
+ ```sh
33
+ npx niral --help
34
+ npx niral --version
35
+ npx niral check myapp
36
+ npx niral test myapp
37
+ npx niral build myapp
38
+ npx niral start myapp
39
+ npx niral add api myapp
40
+ ```
41
+
42
+ All framework commands and arguments are forwarded, with the framework's exit
43
+ status preserved. See the [complete CLI reference](https://niral.site/docs/cli).
44
+ `--version` reports this launcher and, when installed, the framework version.
45
+ The generated direct `niral` shim reports the framework version only.
46
+
47
+ ## Update
48
+
49
+ Launcher 0.1.3 supports explicit framework updates:
50
+
51
+ ```sh
52
+ npx niral@latest update
53
+ ```
54
+
55
+ This explicitly updates `NIRAL_HOME/framework` from the repository's `main`
56
+ branch, even when the cached framework has no `update` command. After updating,
57
+ the direct `niral update` command is available too. Updates follow `main`, not
58
+ a version-pinned release. Updating the npm launcher alone does not refresh the
59
+ framework; update a globally installed launcher separately with
60
+ `npm install -g niral@latest`.
61
+
62
+ Git installs must be clean, on `main`, and use the official HTTPS origin. Updates
63
+ are fast-forward only; a Git failure never replaces the checkout with an archive.
64
+ Non-Git replacements are staged and syntax-checked first; the complete previous
65
+ directory is retained at the printed `framework-backup-*/framework` path. Local
66
+ files in that backup are not merged into the new framework. No backups are pruned.
67
+
68
+ Stop active development/build commands before updating. Concurrent installers are
69
+ refused. If interrupted, inspect other installer processes before manually removing
70
+ a stale `NIRAL_HOME/.framework-install-lock` directory. Symlinked installs and
71
+ updates invoked from unmanaged source checkouts are refused; maintain those with
72
+ Git. An update does not migrate apps, publish packages or deploy/restart servers.
73
+
74
+ ## Developer Intelligence
75
+
76
+ ```sh
77
+ npx niral di setup --help
78
+ npx niral di status
79
+ npx niral di ask "Explain this project" --root ./myapp --read-only
80
+ npx niral di chat --root ./myapp
81
+ npx niral di new myblog "Customize the home page" --template blog
82
+ ```
83
+
84
+ Configure your exact protocol, API base, model/deployment and auth with `di setup`.
85
+ Credentials stay in a named environment variable, not config or project files.
86
+ File reads/edits require a terminal and per-tool approval; writes retain backups
87
+ and reject stale hashes. `ask <prompt> --no-tools` permits prompt-only automation.
88
+ No unrestricted shell, automatic tests, transcript persistence or Niral Cloud
89
+ requirement. See the [DI commands and safeguards](https://niral.site/docs/cli).
90
+ Existing cached frameworks need `npx niral@latest update` to acquire DI commands.
@@ -1,21 +1,35 @@
1
1
  #!/usr/bin/env node
2
- // npx create-niral my-app — the beginner's one command.
3
- import { ensureFramework, installLauncher, runNiral } from "../index.js";
2
+ // npx create-niral my-app [--template blog|dashboard] — the beginner's one command.
3
+ import { ensureFramework, installLauncher, runNiral, runLauncher } from "../index.js";
4
4
 
5
- const name = process.argv[2];
6
- if (!name || name.startsWith("-")) {
7
- console.error("usage: npx create-niral <app-name>");
8
- process.exit(1);
9
- }
5
+ const usage = "usage: npx create-niral <app-name|.> [--template minimal|blog|dashboard]";
6
+
7
+ async function main(argv = process.argv.slice(2)) {
8
+ if (argv.includes("--help") || argv.includes("-h")) {
9
+ console.log(usage);
10
+ return 0;
11
+ }
12
+ if (argv.length === 1 && ["--version", "-v"].includes(argv[0])) return runLauncher(["--version"]);
13
+ const name = argv[0];
14
+ if (!name || (name.startsWith("-") && name !== ".")) throw new Error(usage);
15
+ if (argv.length !== 1 && !(argv.length === 3 && argv[1] === "--template" && ["minimal", "blog", "dashboard"].includes(argv[2]))) throw new Error(usage);
10
16
 
11
- await ensureFramework();
12
- const code = runNiral(["create", name]);
13
- if (code !== 0) process.exit(code);
17
+ await ensureFramework();
18
+ const code = runNiral(["create", name, ...argv.slice(1)]);
19
+ if (code !== 0) return code;
20
+
21
+ const { profile, added } = installLauncher();
22
+ console.log("");
23
+ if (added) console.log(`✓ \`niral\` command installed — PATH updated in ${profile} (open a new terminal)`);
24
+ if (!profile) console.log("Use npx niral <command>, or add NIRAL_HOME/bin to your PATH.");
25
+ const cd = name === "." ? "" : ` cd ${name}\n`;
26
+ console.log(`Next:\n${cd} niral dev # or: npx niral dev\n`);
27
+ return 0;
28
+ }
14
29
 
15
- const { profile, added } = installLauncher();
16
- console.log("");
17
- if (added) console.log(`✓ \`niral\` command installed — PATH updated in ${profile} (open a new terminal)`);
18
- console.log(`Next:
19
- cd ${name}
20
- niral dev # or: npx niral dev
21
- `);
30
+ main().then((code) => {
31
+ process.exitCode = code;
32
+ }).catch((error) => {
33
+ console.error(`create-niral: ${error?.message || error}`);
34
+ process.exitCode = 1;
35
+ });
package/bin/niral.js CHANGED
@@ -1,7 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  // niral <command> — forwards to the framework at ~/.niral/framework.
3
3
  // Available globally after `npm i -g create-niral`, or per-call via `npx niral dev`.
4
- import { ensureFramework, runNiral } from "../index.js";
4
+ import { runLauncher } from "../index.js";
5
5
 
6
- await ensureFramework();
7
- process.exit(runNiral(process.argv.slice(2)));
6
+ async function main() {
7
+ return runLauncher(process.argv.slice(2));
8
+ }
9
+
10
+ main().then((code) => {
11
+ process.exitCode = code;
12
+ }).catch((error) => {
13
+ console.error(`niral: ${error?.message || error}`);
14
+ process.exitCode = 1;
15
+ });
package/index.js CHANGED
@@ -6,81 +6,199 @@
6
6
  * forwards commands to it. The framework itself never touches npm.
7
7
  */
8
8
 
9
- import { existsSync, mkdirSync, rmSync, writeFileSync, chmodSync, appendFileSync, readFileSync } from "node:fs";
10
- import { spawnSync, execSync } from "node:child_process";
11
- import { join } from "node:path";
9
+ import { existsSync, mkdirSync, mkdtempSync, renameSync, lstatSync, realpathSync, rmSync, writeFileSync, chmodSync, appendFileSync, readFileSync } from "node:fs";
10
+ import { spawnSync } from "node:child_process";
11
+ import { join, resolve } from "node:path";
12
12
  import { homedir } from "node:os";
13
13
  import { get } from "node:https";
14
14
 
15
- export const NIRAL_HOME = process.env.NIRAL_HOME || join(homedir(), ".niral");
15
+ export const NIRAL_HOME = resolve(process.env.NIRAL_HOME || join(homedir(), ".niral"));
16
16
  const FRAMEWORK = join(NIRAL_HOME, "framework");
17
17
  const TARBALL = "https://codeload.github.com/Zyora-Dev/niral/tar.gz/refs/heads/main";
18
18
  const REPO = "https://github.com/Zyora-Dev/niral.git";
19
19
 
20
20
  function download(url, dest) {
21
21
  return new Promise((resolve, reject) => {
22
- get(url, (res) => {
23
- if (res.statusCode !== 200) return reject(new Error(`download failed: HTTP ${res.statusCode}`));
22
+ const request = get(url, (res) => {
23
+ if (res.statusCode !== 200) {
24
+ res.resume();
25
+ return reject(new Error(`download failed: HTTP ${res.statusCode}`));
26
+ }
24
27
  const chunks = [];
25
- res.on("data", (c) => chunks.push(c));
28
+ let bytes = 0;
29
+ res.on("data", (chunk) => {
30
+ bytes += chunk.length;
31
+ if (bytes > 64 * 1024 * 1024) {
32
+ request.destroy(new Error("framework download exceeds 64 MiB"));
33
+ return;
34
+ }
35
+ chunks.push(chunk);
36
+ });
26
37
  res.on("end", () => {
27
- writeFileSync(dest, Buffer.concat(chunks));
28
- resolve();
38
+ try {
39
+ writeFileSync(dest, Buffer.concat(chunks));
40
+ resolve();
41
+ } catch (error) {
42
+ reject(error);
43
+ }
29
44
  });
30
45
  res.on("error", reject);
31
46
  }).on("error", reject);
47
+ const deadline = setTimeout(() => request.destroy(new Error("framework download timed out")), 60000);
48
+ request.on("close", () => clearTimeout(deadline));
32
49
  });
33
50
  }
34
51
 
35
52
  function hasGit() {
36
- try {
37
- execSync("git --version", { stdio: "ignore" });
38
- return true;
39
- } catch {
40
- return false;
41
- }
53
+ const result = spawnSync("git", ["--version"], { stdio: "ignore" });
54
+ return !result.error && result.status === 0;
55
+ }
56
+
57
+ function commandError(label, result) {
58
+ const detail = result.error?.message || result.stderr?.trim() || `exit ${result.status ?? "unknown"}`;
59
+ return new Error(`${label} failed: ${detail}`);
60
+ }
61
+
62
+ async function installFromTarball(stage) {
63
+ const tgz = join(stage, "niral.tgz");
64
+ await download(TARBALL, tgz);
65
+ const target = join(stage, "framework");
66
+ mkdirSync(target);
67
+ const extracted = spawnSync("tar", ["-xzf", tgz, "-C", target, "--strip-components=1"], { encoding: "utf8", timeout: 120000 });
68
+ rmSync(tgz, { force: true });
69
+ if (extracted.error || extracted.status !== 0) throw commandError("framework extraction", extracted);
42
70
  }
43
71
 
44
72
  /** Download the framework to ~/.niral/framework (git if available, tarball otherwise). */
45
73
  export async function ensureFramework({ update = false } = {}) {
74
+ if (Number(process.versions.node.split(".")[0]) < 22) throw new Error("Niral requires Node.js 22 or newer");
46
75
  if (existsSync(join(FRAMEWORK, "bin", "niral.js")) && !update) return FRAMEWORK;
47
- console.log("niral · downloading the framework (one time, ~1 MB — it has zero dependencies)…");
48
76
  mkdirSync(NIRAL_HOME, { recursive: true });
49
- if (hasGit()) {
77
+ const lock = join(NIRAL_HOME, ".framework-install-lock");
78
+ try {
79
+ mkdirSync(lock);
80
+ } catch (error) {
81
+ if (error.code === "EEXIST") throw new Error(`framework installation is locked: ${lock}. Check for another installer before removing a stale lock.`);
82
+ throw error;
83
+ }
84
+ let stage;
85
+ try {
86
+ if (existsSync(join(FRAMEWORK, "bin", "niral.js")) && !update) return FRAMEWORK;
87
+ const entry = lstatSync(FRAMEWORK, { throwIfNoEntry: false });
88
+ if (entry && (!entry.isDirectory() || entry.isSymbolicLink())) throw new Error("framework path must be a real directory; installation left unchanged");
89
+ console.log(update ? "niral: updating the installed framework..." : "niral: downloading the framework...");
50
90
  if (existsSync(join(FRAMEWORK, ".git"))) {
51
- execSync("git pull -q", { cwd: FRAMEWORK, stdio: "ignore" });
52
- } else {
53
- rmSync(FRAMEWORK, { recursive: true, force: true });
54
- execSync(`git clone -q --depth 1 ${REPO} "${FRAMEWORK}"`, { stdio: "ignore" });
91
+ if (!hasGit()) throw new Error("Git is required to update this checkout; installation left unchanged");
92
+ const git = (args) => {
93
+ const result = spawnSync("git", args, { cwd: FRAMEWORK, encoding: "utf8", timeout: 120000, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
94
+ if (result.error || result.status !== 0) throw commandError("Git update (no archive fallback)", result);
95
+ return result.stdout.trim();
96
+ };
97
+ if (git(["status", "--porcelain", "--untracked-files=all"])) throw new Error("framework checkout has local changes; preserve them before updating");
98
+ if (git(["branch", "--show-current"]) !== "main") throw new Error("framework checkout is not on main; update it manually with Git");
99
+ if (![REPO, REPO.replace(/\.git$/, "")].includes(git(["remote", "get-url", "origin"]))) throw new Error("framework origin differs from the official repository; update it manually with Git");
100
+ const previous = git(["rev-parse", "HEAD"]);
101
+ git(["pull", "--ff-only", "origin", "main"]);
102
+ console.log(`niral: framework ${git(["rev-parse", "--short", "HEAD"])} (previous ${previous})`);
103
+ return FRAMEWORK;
55
104
  }
56
- } else {
57
- const tgz = join(NIRAL_HOME, "niral.tgz");
58
- await download(TARBALL, tgz);
59
- rmSync(FRAMEWORK, { recursive: true, force: true });
60
- mkdirSync(FRAMEWORK, { recursive: true });
61
- execSync(`tar -xzf "${tgz}" -C "${FRAMEWORK}" --strip-components=1`, { stdio: "ignore" });
62
- rmSync(tgz);
105
+ stage = mkdtempSync(join(NIRAL_HOME, ".framework-stage-"));
106
+ const target = join(stage, "framework");
107
+ let installed = false;
108
+ if (hasGit()) {
109
+ const cloned = spawnSync("git", ["clone", "-q", "--depth", "1", "--branch", "main", REPO, target], {
110
+ encoding: "utf8", timeout: 120000, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
111
+ });
112
+ installed = !cloned.error && cloned.status === 0;
113
+ if (!installed) {
114
+ rmSync(target, { recursive: true, force: true });
115
+ console.warn("niral: Git download unavailable; trying the framework archive...");
116
+ }
117
+ }
118
+ if (!installed) await installFromTarball(stage);
119
+ const manifest = JSON.parse(readFileSync(join(target, "package.json"), "utf8"));
120
+ if (manifest.name !== "niral" || !existsSync(join(target, "src", "index.js"))) throw new Error("download is not a Niral framework");
121
+ const checked = spawnSync(process.execPath, ["--check", join(target, "bin", "niral.js")], { encoding: "utf8", timeout: 10000 });
122
+ if (checked.error || checked.status !== 0) throw commandError("downloaded CLI validation", checked);
123
+ let backup;
124
+ if (entry) {
125
+ backup = join(mkdtempSync(join(NIRAL_HOME, "framework-backup-")), "framework");
126
+ renameSync(FRAMEWORK, backup);
127
+ }
128
+ try {
129
+ renameSync(target, FRAMEWORK);
130
+ } catch (error) {
131
+ if (backup) renameSync(backup, FRAMEWORK);
132
+ throw error;
133
+ }
134
+ if (backup) console.log(`niral: previous framework preserved at ${backup}`);
135
+ console.log(`niral: framework ${manifest.version} ready`);
136
+ return FRAMEWORK;
137
+ } finally {
138
+ if (stage) rmSync(stage, { recursive: true, force: true });
139
+ rmSync(lock, { recursive: true, force: true });
140
+ }
141
+ }
142
+
143
+ export async function updateFramework({ sourceRoot } = {}) {
144
+ if (sourceRoot && (!existsSync(FRAMEWORK) || realpathSync(sourceRoot) !== realpathSync(FRAMEWORK))) {
145
+ throw new Error("niral update only updates the managed NIRAL_HOME/framework installation. Update source checkouts with Git; deploy servers separately.");
63
146
  }
64
- if (!existsSync(join(FRAMEWORK, "bin", "niral.js"))) throw new Error("framework download failed — try again or clone github.com/Zyora-Dev/niral manually");
65
- return FRAMEWORK;
147
+ await ensureFramework({ update: true });
148
+ }
149
+
150
+ export async function runLauncher(args) {
151
+ if (args[0] === "update") {
152
+ if (args.length === 2 && ["--help", "-h"].includes(args[1])) {
153
+ console.log("usage: niral update\nUpdates NIRAL_HOME/framework from main; does not update npm packages or deploy apps.");
154
+ return 0;
155
+ }
156
+ if (args.length !== 1) throw new Error("usage: niral update");
157
+ await updateFramework();
158
+ return 0;
159
+ }
160
+ if (!args.length || ["help", "--help", "-h"].includes(args[0])) {
161
+ if (existsSync(join(FRAMEWORK, "bin", "niral.js"))) return runNiral(args);
162
+ console.log("niral <command> [options]\nCommands: create, dev, build, start, check, test, add, doctor, di, update.\nThe first framework command downloads the framework.\nniral di --help: Developer Intelligence setup, status, ask, chat, new.\nniral update: explicitly refresh the installed framework.\nRun niral --help after installation for the full framework command list.");
163
+ return 0;
164
+ }
165
+ if (["--version", "-v", "version"].includes(args[0])) {
166
+ const manifest = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8"));
167
+ console.log(`${manifest.name} launcher ${manifest.version}`);
168
+ if (existsSync(join(FRAMEWORK, "package.json"))) {
169
+ const framework = JSON.parse(readFileSync(join(FRAMEWORK, "package.json"), "utf8"));
170
+ console.log(`niral framework ${framework.version}`);
171
+ }
172
+ return 0;
173
+ }
174
+ await ensureFramework();
175
+ return runNiral(args);
66
176
  }
67
177
 
68
178
  /** Put a `niral` launcher on the PATH so every later command is just `niral dev`. */
69
179
  export function installLauncher() {
70
180
  const binDir = join(NIRAL_HOME, "bin");
71
181
  mkdirSync(binDir, { recursive: true });
72
- const shim = join(binDir, "niral");
73
- writeFileSync(shim, `#!/usr/bin/env bash\nexec node "${join(FRAMEWORK, "bin", "niral.js")}" "$@"\n`);
74
- chmodSync(shim, 0o755);
182
+ const frameworkCli = join(FRAMEWORK, "bin", "niral.js");
183
+ const shim = join(binDir, process.platform === "win32" ? "niral.cmd" : "niral");
184
+ if (process.platform === "win32") {
185
+ writeFileSync(shim, `@echo off\r\nset "NIRAL_HOME=${NIRAL_HOME.replaceAll("%", "%%")}"\r\n"${process.execPath.replaceAll("%", "%%")}" "${frameworkCli.replaceAll("%", "%%")}" %*\r\n`);
186
+ } else {
187
+ const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
188
+ writeFileSync(shim, `#!/usr/bin/env sh\nexport NIRAL_HOME=${quote(NIRAL_HOME)}\nexec ${quote(process.execPath)} ${quote(frameworkCli)} "$@"\n`);
189
+ chmodSync(shim, 0o755);
190
+ }
75
191
 
76
192
  const profile =
77
193
  (process.env.SHELL ?? "").endsWith("/zsh") ? join(homedir(), ".zshrc")
78
194
  : (process.env.SHELL ?? "").endsWith("/bash") ? join(homedir(), ".bashrc")
79
195
  : null;
80
196
  if (profile) {
81
- const line = `export PATH="$HOME/.niral/bin:$PATH"`;
197
+ const line = NIRAL_HOME === join(homedir(), ".niral")
198
+ ? `export PATH="$HOME/.niral/bin:$PATH"`
199
+ : `export PATH='${binDir.replaceAll("'", "'\\''")}':"$PATH"`;
82
200
  const current = existsSync(profile) ? readFileSync(profile, "utf8") : "";
83
- if (!current.includes(".niral/bin")) {
201
+ if (!current.split(/\r?\n/).includes(line)) {
84
202
  appendFileSync(profile, `\n# niral\n${line}\n`);
85
203
  return { shim, profile, added: true };
86
204
  }
@@ -91,6 +209,9 @@ export function installLauncher() {
91
209
 
92
210
  /** Forward a command to the framework CLI. */
93
211
  export function runNiral(args) {
94
- const r = spawnSync("node", [join(FRAMEWORK, "bin", "niral.js"), ...args], { stdio: "inherit" });
95
- return r.status ?? 0;
212
+ const r = spawnSync(process.execPath, [join(FRAMEWORK, "bin", "niral.js"), ...args], { stdio: "inherit" });
213
+ if (r.error) throw new Error(`could not start the Niral CLI: ${r.error.message}`);
214
+ if (r.signal) throw new Error(`Niral CLI terminated by ${r.signal}`);
215
+ if (r.status === null) throw new Error("Niral CLI exited without a status");
216
+ return r.status;
96
217
  }
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "niral",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Niral — the zero-dependency full-stack framework by ZyoraLabs. This package is a tiny launcher; the framework itself lives at github.com/Zyora-Dev/niral and has zero dependencies.",
5
5
  "bin": {
6
6
  "niral": "bin/niral.js",
7
7
  "create-niral": "bin/create-niral.js"
8
8
  },
9
9
  "type": "module",
10
+ "files": ["bin/", "index.js", "README.md"],
10
11
  "engines": { "node": ">=22" },
11
12
  "keywords": ["niral", "framework", "fullstack", "zero-dependencies", "ssr", "zyoralabs"],
12
- "homepage": "https://niral.zyora.club",
13
+ "homepage": "https://niral.site",
13
14
  "repository": { "type": "git", "url": "git+https://github.com/Zyora-Dev/niral.git" },
14
15
  "license": "MIT",
15
16
  "dependencies": {}