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.
- package/cli.js +92 -0
- package/index.js +185 -0
- 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
|
+
}
|