producs 1.0.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.
Files changed (3) hide show
  1. package/cli.js +92 -0
  2. package/index.js +185 -0
  3. package/package.json +20 -0
package/cli.js ADDED
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ const { Command } = require("commander");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const chalk = require("chalk");
6
+ const { ProDucs } = require("./producs.js");
7
+
8
+ const PAGES_DIR = "/usr/share/producs";
9
+ const program = new Command();
10
+
11
+ // ───────────────────────────────────────────────
12
+ // Utility functions
13
+ // ───────────────────────────────────────────────
14
+ function listPages(showDesc = false) {
15
+ if (!fs.existsSync(PAGES_DIR)) {
16
+ console.log(chalk.red(`No pages directory found at ${PAGES_DIR}`));
17
+ return;
18
+ }
19
+
20
+ const files = fs.readdirSync(PAGES_DIR).filter(f => f.endsWith(".prd"));
21
+ if (files.length === 0) {
22
+ console.log(chalk.yellow("No .prd pages found in"), PAGES_DIR);
23
+ return;
24
+ }
25
+
26
+ console.log(chalk.bold.cyan("Available ProDucs pages:\n"));
27
+ for (const file of files) {
28
+ const name = file.replace(/\.prd$/, "");
29
+ let desc = "";
30
+ if (showDesc) {
31
+ try {
32
+ const firstLine = fs.readFileSync(path.join(PAGES_DIR, file), "utf8").split("\n")[0];
33
+ desc = " — " + firstLine.trim();
34
+ } catch {}
35
+ }
36
+ console.log(" • " + chalk.green(name) + chalk.gray(desc));
37
+ }
38
+ }
39
+
40
+ // ───────────────────────────────────────────────
41
+ // Page runner
42
+ // ───────────────────────────────────────────────
43
+ function runPage(name) {
44
+ const filePath = path.join(PAGES_DIR, name.endsWith(".prd") ? name : name + ".prd");
45
+ if (!fs.existsSync(filePath)) {
46
+ console.error(chalk.red(`Page not found:`), filePath);
47
+ console.log();
48
+ listPages(true);
49
+ process.exit(1);
50
+ }
51
+
52
+ const src = fs.readFileSync(filePath, "utf8");
53
+ const engine = new ProDucs();
54
+
55
+ console.log(chalk.yellowBright(`\n── Running page: ${name} ──\n`));
56
+ try {
57
+ const ast = engine.astify(src);
58
+ engine.execute(ast);
59
+ console.log(chalk.greenBright("\n✔ Page completed successfully.\n"));
60
+ } catch (err) {
61
+ console.error(chalk.red.bold("\n✖ Execution error:"), err.message, "\n");
62
+ process.exit(1);
63
+ }
64
+ }
65
+
66
+ // ───────────────────────────────────────────────
67
+ // CLI setup with commander
68
+ // ───────────────────────────────────────────────
69
+ program
70
+ .name("producs")
71
+ .description("Interactive documentation and tutorial runner")
72
+ .version("1.0.0");
73
+
74
+ program
75
+ .command("list")
76
+ .description("List all available ProDucs pages")
77
+ .option("-d, --describe", "Show first-line descriptions for each page")
78
+ .action((options) => listPages(options.describe));
79
+
80
+ program
81
+ .command("run <page>")
82
+ .description("Run a specific ProDucs documentation page")
83
+ .action(runPage);
84
+
85
+ program
86
+ .argument("[page]", "Run a page directly (shortcut for 'run <page>')")
87
+ .action((page) => {
88
+ if (page) runPage(page);
89
+ else listPages(true);
90
+ });
91
+
92
+ program.parse(process.argv);
package/index.js ADDED
@@ -0,0 +1,185 @@
1
+ const { execSync } = require("child_process");
2
+ const prompt = require("prompt-sync")({ sigint: true });
3
+
4
+ class ProDucs {
5
+ constructor() {
6
+ this.commands = {
7
+ ">": (text, value, promptText = "") => {
8
+ const result = prompt(promptText || text || "");
9
+ return { VALUE: result, failed: result ? 0 : 1 };
10
+ },
11
+ Check_equals: (text, value, expected) => ({
12
+ VALUE: value.VALUE,
13
+ failed: value.VALUE === expected ? 0 : 1
14
+ }),
15
+ Check_failed_error: (text, value, msg) => {
16
+ if (value.failed) throw new Error(msg || "Check failed!");
17
+ return value;
18
+ },
19
+ Shell_run: (text, value) => {
20
+ execSync(value.VALUE, { stdio: "inherit" });
21
+ return { VALUE: value.VALUE, failed: 0 };
22
+ },
23
+ Text: (text, value, str) => ({ VALUE: str || text, failed: 0 })
24
+ };
25
+ }
26
+
27
+ // ──────────────────────────── TOKENIZER ────────────────────────────
28
+ tokenize(src) {
29
+ const tokens = [];
30
+ let i = 0;
31
+ const peek = () => src[i];
32
+ const next = () => src[i++];
33
+ const eof = () => i >= src.length;
34
+ const skipSpaces = () => { while (!eof() && /\s/.test(peek())) next(); };
35
+
36
+ const readIdent = () => {
37
+ let name = "";
38
+ while (!eof() && /[A-Za-z0-9_>]/.test(peek())) name += next();
39
+ return name;
40
+ };
41
+
42
+ const readArgs = () => {
43
+ const args = [];
44
+ let buf = "";
45
+ if (peek() === "[") {
46
+ next();
47
+ while (!eof()) {
48
+ const c = next();
49
+ if (c === "]") break;
50
+ if (c === ",") {
51
+ args.push(buf.trim().replace(/^'|'$/g, ""));
52
+ buf = "";
53
+ } else buf += c;
54
+ }
55
+ if (buf.trim()) args.push(buf.trim().replace(/^'|'$/g, ""));
56
+ }
57
+ return args;
58
+ };
59
+
60
+ while (!eof()) {
61
+ skipSpaces();
62
+
63
+ if (peek() === "#" && src[i + 1] === "$") {
64
+ i += 2;
65
+ if (src.startsWith("End_", i)) {
66
+ i += 4;
67
+ const name = readIdent();
68
+ tokens.push({ type: "COMMAND_END", name });
69
+ } else {
70
+ const name = readIdent();
71
+ const args = readArgs();
72
+ tokens.push({ type: "COMMAND_START", name, args });
73
+ }
74
+ } else {
75
+ let text = "";
76
+ while (!eof() && !(peek() === "#" && src[i + 1] === "$")) text += next();
77
+ if (text.trim()) tokens.push({ type: "TEXT", value: text.trim() });
78
+ }
79
+ }
80
+
81
+ return tokens;
82
+ }
83
+
84
+ // ──────────────────────────── PARSER ────────────────────────────
85
+ parse(tokens) {
86
+ const root = [];
87
+ const stack = [];
88
+
89
+ for (let i = 0; i < tokens.length; i++) {
90
+ const token = tokens[i];
91
+
92
+ if (token.type === "COMMAND_START") {
93
+ const node = {
94
+ type: token.name,
95
+ args: token.args,
96
+ children: [],
97
+ pipedCommands: []
98
+ };
99
+
100
+ // Inline (pipe) commands
101
+ if (stack.length && !token.name.startsWith("End_")) {
102
+ const parent = stack.at(-1);
103
+ if (parent.type === ">" || parent.pipedCommands.length) {
104
+ parent.pipedCommands.push(node);
105
+ continue;
106
+ }
107
+ }
108
+
109
+ // Block commands
110
+ stack.push(node);
111
+
112
+ } else if (token.type === "COMMAND_END") {
113
+ const last = stack.pop();
114
+ if (!last)
115
+ throw new Error(`Unexpected #$End_${token.name}`);
116
+ if (last.type !== token.name)
117
+ throw new Error(`Mismatched end: expected #$End_${last.type}, got #$End_${token.name}`);
118
+ if (stack.length) stack.at(-1).children.push(last);
119
+ else root.push(last);
120
+ } else if (token.type === "TEXT") {
121
+ const node = { type: "Text", value: token.value };
122
+ if (stack.length) stack.at(-1).children.push(node);
123
+ else root.push(node);
124
+ }
125
+ }
126
+
127
+ if (stack.length) throw new Error(`Unclosed command: ${stack.at(-1).type}`);
128
+ return root;
129
+ }
130
+
131
+ astify(text) {
132
+ return this.parse(this.tokenize(text));
133
+ }
134
+
135
+ // ──────────────────────────── EXECUTION ────────────────────────────
136
+ executeNode(node, input = { VALUE: "", failed: 0 }) {
137
+ // Handle text nodes directly
138
+ if (node.type === "Text")
139
+ return { VALUE: console.log(node.value), failed: 0 };
140
+
141
+ const cmd = this.commands[node.type];
142
+ if (!cmd) throw new Error(`Unknown command: ${node.type}`);
143
+
144
+ // Get the first text child as `text`
145
+ const textChild = node.children.find(c => c.type === "Text");
146
+ const text = textChild ? textChild.value : "";
147
+
148
+ // Base execution (text, value, ...args)
149
+ let current = cmd(text, input, ...node.args);
150
+
151
+ // Inline piped commands
152
+ for (const pipe of node.pipedCommands) {
153
+ const fn = this.commands[pipe.type];
154
+ if (!fn) throw new Error(`Unknown piped command: ${pipe.type}`);
155
+ const pipeText = pipe.children.find(c => c.type === "Text")?.value || "";
156
+ current = fn(pipeText, current, ...pipe.args);
157
+ }
158
+
159
+ // Block children
160
+ for (const child of node.children) {
161
+ if (child.type !== "Text") current = this.executeNode(child, current);
162
+ }
163
+
164
+ return current;
165
+ }
166
+
167
+ execute(ast) {
168
+ let result = { VALUE: "", failed: 0 };
169
+ for (const node of ast) {
170
+ result = this.executeNode(node, result);
171
+ }
172
+ return result;
173
+ }
174
+ }
175
+
176
+ // ──────────────────────────── Example ────────────────────────────
177
+ const src = `
178
+ Hi
179
+ #$> Type your command: #$Check_equals['ls -la'] #$Check_failed_error['expected ls -la!'] #$Shell_run #$End_>
180
+ `;
181
+
182
+ const prod = new ProDucs();
183
+ const ast = prod.astify(src);
184
+ console.log("AST:", JSON.stringify(ast, null, 2));
185
+ prod.execute(ast);
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "producs",
3
+ "version": "1.0.1",
4
+ "description": "simple documentation engine",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "bin": {
10
+ "prod": "./cli.js"
11
+ },
12
+ "keywords": [],
13
+ "author": "purcwix",
14
+ "license": "MIT",
15
+ "type": "commonjs",
16
+ "dependencies": {
17
+ "commander": "^14.0.1",
18
+ "prompt-sync": "^4.2.0"
19
+ }
20
+ }