@vsirotin/ts-stop 1.12.1 → 1.13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vsirotin/ts-stop",
3
- "version": "1.12.1",
3
+ "version": "1.13.1",
4
4
  "description": "State Oriented Programming library for TypeScript",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -20,10 +20,21 @@
20
20
  },
21
21
  "files": [
22
22
  "lib",
23
+ "scripts/reduce-fa.js",
24
+ "scripts/merge-fas.js",
25
+ "scripts/merge-fas-from-dir.js",
26
+ "scripts/update-fa.js",
23
27
  "README.md",
24
28
  "LICENSE",
25
29
  "LICENSE-COMMERCIAL.md"
26
30
  ],
31
+ "bin": {
32
+ "reduce-fa": "./scripts/reduce-fa.js",
33
+ "merge-fas": "./scripts/merge-fas.js",
34
+ "merge-fas-from-dir": "./scripts/merge-fas-from-dir.js",
35
+ "update-full-fa": "./scripts/update-fa.js",
36
+ "update-compact-fa": "./scripts/update-fa.js"
37
+ },
27
38
  "scripts": {
28
39
  "build": "npm run build:cjs && npm run build:esm",
29
40
  "build:cjs": "tsc",
@@ -31,10 +42,10 @@
31
42
  "test": "jest",
32
43
  "test:coverage": "jest --coverage",
33
44
  "clean": "rm -rf lib coverage",
34
- "publish:local": "./scripts/publish-local.sh",
35
45
  "pack": "npm run build && npm pack",
36
46
  "reduce-fa": "node scripts/reduce-fa.js",
37
47
  "merge-fas": "node scripts/merge-fas.js",
48
+ "merge-fas-from-dir": "node scripts/merge-fas-from-dir.js",
38
49
  "update-full-fa": "node scripts/update-fa.js --format=full",
39
50
  "update-compact-fa": "node scripts/update-fa.js --format=compact"
40
51
  },
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * merge-fas-from-dir.js — CLI tool to recursively merge all FA JSON files from a directory.
6
+ *
7
+ * Usage:
8
+ * node scripts/merge-fas-from-dir.js --input-dir=<path/to/dir> --result=<path/to/result.json>
9
+ *
10
+ * Behavior:
11
+ * - Recursively finds all .json files in the input directory (including subdirectories).
12
+ * - Sorts files alphabetically by full path for consistent merge order.
13
+ * - Each input file is reduced with reduceFA semantics.
14
+ * - Already compact files are used unchanged.
15
+ * - Duplicate FA keys are overwritten by the latest file and reported as warnings.
16
+ * - Output is a single merged compact FA definition.
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const { mergeFAs } = require('../lib/sfsm');
22
+
23
+ const args = process.argv.slice(2);
24
+ const options = {};
25
+
26
+ for (const arg of args) {
27
+ const match = arg.match(/^--([^=]+)=(.+)$/);
28
+ if (match) {
29
+ options[match[1]] = match[2];
30
+ }
31
+ }
32
+
33
+ if (!options['input-dir']) {
34
+ console.error('Error: --input-dir=<path> is required.');
35
+ console.error('Usage: node scripts/merge-fas-from-dir.js --input-dir=<path/to/dir> --result=<path/to/result.json>');
36
+ process.exit(1);
37
+ }
38
+
39
+ if (!options.result) {
40
+ console.error('Error: --result=<path> is required.');
41
+ console.error('Usage: node scripts/merge-fas-from-dir.js --input-dir=<path/to/dir> --result=<path/to/result.json>');
42
+ process.exit(1);
43
+ }
44
+
45
+ const inputDir = path.resolve(process.cwd(), options['input-dir']);
46
+ const resultFile = path.resolve(process.cwd(), options.result);
47
+
48
+ // Verify input directory exists
49
+ if (!fs.existsSync(inputDir) || !fs.statSync(inputDir).isDirectory()) {
50
+ console.error(`Error: Input directory not found or not a directory: ${inputDir}`);
51
+ process.exit(1);
52
+ }
53
+
54
+ /**
55
+ * Recursively find all .json files in a directory.
56
+ */
57
+ function findJsonFiles(dir, baseDir = dir) {
58
+ const files = [];
59
+ const entries = fs.readdirSync(dir);
60
+
61
+ for (const entry of entries) {
62
+ const fullPath = path.join(dir, entry);
63
+ const stat = fs.statSync(fullPath);
64
+
65
+ if (stat.isDirectory()) {
66
+ files.push(...findJsonFiles(fullPath, baseDir));
67
+ } else if (entry.endsWith('.json')) {
68
+ files.push(fullPath);
69
+ }
70
+ }
71
+
72
+ return files;
73
+ }
74
+
75
+ /**
76
+ * Check if a definition is in compact format.
77
+ */
78
+ function isCompactDefinition(definition) {
79
+ const entries = Object.entries(definition);
80
+ return entries.length > 0 && entries.every(([, value]) => Array.isArray(value));
81
+ }
82
+
83
+ // Find all JSON files
84
+ const jsonFiles = findJsonFiles(inputDir).sort();
85
+
86
+ if (jsonFiles.length === 0) {
87
+ console.error(`Error: No JSON files found in input directory: ${inputDir}`);
88
+ process.exit(1);
89
+ }
90
+
91
+ console.log(`Found ${jsonFiles.length} JSON file(s) in ${inputDir}`);
92
+
93
+ const definitions = [];
94
+
95
+ for (let i = 0; i < jsonFiles.length; i++) {
96
+ const file = jsonFiles[i];
97
+ const relPath = path.relative(inputDir, file);
98
+ let definition;
99
+
100
+ try {
101
+ definition = JSON.parse(fs.readFileSync(file, 'utf-8'));
102
+ } catch (err) {
103
+ console.error(`Error: Failed to parse JSON from ${file}: ${err.message}`);
104
+ process.exit(1);
105
+ }
106
+
107
+ if (isCompactDefinition(definition)) {
108
+ console.log(`Info: Input #${i + 1} is already compact and will be used as-is: ${relPath}`);
109
+ } else {
110
+ console.log(`Info: Input #${i + 1} is extended and will be reduced: ${relPath}`);
111
+ }
112
+
113
+ definitions.push(definition);
114
+ }
115
+
116
+ const { merged, warnings } = mergeFAs(definitions);
117
+
118
+ for (const warning of warnings) {
119
+ console.warn(`Warning: ${warning}`);
120
+ }
121
+
122
+ const json = JSON.stringify(merged, null, 4).replace(
123
+ /\[\s*\n\s*"([^"]+)",\s*\n\s*"([^"]+)",\s*\n\s*"([^"]+)"(?:,\s*\n\s*"([^"]+)")?\s*\n\s*\]/g,
124
+ (_, a, b, c, d) => d ? `["${a}", "${b}", "${c}", "${d}"]` : `["${a}", "${b}", "${c}"]`
125
+ );
126
+
127
+ fs.mkdirSync(path.dirname(resultFile), { recursive: true });
128
+ fs.writeFileSync(resultFile, `${json}\n`, 'utf-8');
129
+ console.log(`Merged compact FA written to: ${resultFile}`);
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * merge-fas.js — CLI tool to reduce and merge multiple FA JSON files.
6
+ *
7
+ * Usage:
8
+ * npm run merge-fas -- --result=<path/to/result.json> <file1[,file2,...]> [file3 ...]
9
+ *
10
+ * Behavior:
11
+ * - Each input file is reduced with reduceFA semantics.
12
+ * - Already compact files are used unchanged.
13
+ * - Duplicate FA keys are overwritten by the latest file and reported as warnings.
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { mergeFAs } = require('../lib/sfsm');
19
+
20
+ const args = process.argv.slice(2);
21
+ const options = {};
22
+ const rawFiles = [];
23
+
24
+ for (const arg of args) {
25
+ const match = arg.match(/^--([^=]+)=(.+)$/);
26
+ if (match) {
27
+ options[match[1]] = match[2];
28
+ } else {
29
+ rawFiles.push(arg);
30
+ }
31
+ }
32
+
33
+ if (!options.result) {
34
+ console.error('Error: --result=<path> is required.');
35
+ console.error('Usage: npm run merge-fas -- --result=<path/to/result.json> <file1[,file2,...]> [file3 ...]');
36
+ process.exit(1);
37
+ }
38
+
39
+ const inputFiles = rawFiles
40
+ .flatMap((chunk) => chunk.split(','))
41
+ .map((s) => s.trim())
42
+ .filter(Boolean);
43
+
44
+ if (inputFiles.length === 0) {
45
+ console.error('Error: At least one input file is required.');
46
+ console.error('Usage: npm run merge-fas -- --result=<path/to/result.json> <file1[,file2,...]> [file3 ...]');
47
+ process.exit(1);
48
+ }
49
+
50
+ const resolvedInputFiles = inputFiles.map((f) => path.resolve(process.cwd(), f));
51
+ const resultFile = path.resolve(process.cwd(), options.result);
52
+
53
+ for (const file of resolvedInputFiles) {
54
+ if (!fs.existsSync(file)) {
55
+ console.error(`Error: Input file not found: ${file}`);
56
+ process.exit(1);
57
+ }
58
+ }
59
+
60
+ function isCompactDefinition(definition) {
61
+ const entries = Object.entries(definition);
62
+ return entries.length > 0 && entries.every(([, value]) => Array.isArray(value));
63
+ }
64
+
65
+ const definitions = [];
66
+
67
+ for (let i = 0; i < resolvedInputFiles.length; i++) {
68
+ const file = resolvedInputFiles[i];
69
+ let definition;
70
+ try {
71
+ definition = JSON.parse(fs.readFileSync(file, 'utf-8'));
72
+ } catch (err) {
73
+ console.error(`Error: Failed to parse JSON from ${file}: ${err.message}`);
74
+ process.exit(1);
75
+ }
76
+
77
+ if (isCompactDefinition(definition)) {
78
+ console.log(`Info: Input #${i + 1} is already compact and will be used as-is: ${file}`);
79
+ } else {
80
+ console.log(`Info: Input #${i + 1} is extended and will be reduced: ${file}`);
81
+ }
82
+
83
+ definitions.push(definition);
84
+ }
85
+
86
+ const { merged, warnings } = mergeFAs(definitions);
87
+
88
+ for (const warning of warnings) {
89
+ console.warn(`Warning: ${warning}`);
90
+ }
91
+
92
+ const json = JSON.stringify(merged, null, 4).replace(
93
+ /\[\s*\n\s*"([^"]+)",\s*\n\s*"([^"]+)",\s*\n\s*"([^"]+)"(?:,\s*\n\s*"([^"]+)")?\s*\n\s*\]/g,
94
+ (_, a, b, c, d) => d ? `["${a}", "${b}", "${c}", "${d}"]` : `["${a}", "${b}", "${c}"]`
95
+ );
96
+
97
+ fs.mkdirSync(path.dirname(resultFile), { recursive: true });
98
+ fs.writeFileSync(resultFile, `${json}\n`, 'utf-8');
99
+ console.log(`Merged compact FA written to: ${resultFile}`);
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * reduce-fa.js — CLI tool to convert an extended FA JSON file to compact format.
6
+ *
7
+ * Usage:
8
+ * npm run reduce-fa -- <path/to/extended-fa.json>
9
+ *
10
+ * Output:
11
+ * Writes the compact definition to <input>-compact.json in the same directory.
12
+ *
13
+ * Requires the library to be built first: npm run build
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { reduceFA } = require('../lib/sfsm');
19
+
20
+ const inputArg = process.argv[2];
21
+
22
+ if (!inputArg) {
23
+ console.error('Error: No input file specified.');
24
+ console.error('Usage: npm run reduce-fa -- <path/to/extended-fa.json>');
25
+ process.exit(1);
26
+ }
27
+
28
+ const inputFile = path.resolve(process.cwd(), inputArg);
29
+
30
+ if (!fs.existsSync(inputFile)) {
31
+ console.error(`Error: File not found: ${inputFile}`);
32
+ process.exit(1);
33
+ }
34
+
35
+ let definition;
36
+ try {
37
+ definition = JSON.parse(fs.readFileSync(inputFile, 'utf-8'));
38
+ } catch (err) {
39
+ console.error(`Error: Failed to parse JSON from ${inputFile}: ${err.message}`);
40
+ process.exit(1);
41
+ }
42
+
43
+ const compact = reduceFA(definition);
44
+
45
+ const ext = path.extname(inputFile);
46
+ const base = path.basename(inputFile, ext);
47
+ const dir = path.dirname(inputFile);
48
+ const outputFile = path.join(dir, `${base}-compact${ext}`);
49
+
50
+ // Serialize with transitions inline: ["state", "signal", "newState", "cmd?"]
51
+ const json = JSON.stringify(compact, null, 4).replace(
52
+ /\[\s*\n\s*"([^"]+)",\s*\n\s*"([^"]+)",\s*\n\s*"([^"]+)"(?:,\s*\n\s*"([^"]+)")?\s*\n\s*\]/g,
53
+ (_, a, b, c, d) => d ? `["${a}", "${b}", "${c}", "${d}"]` : `["${a}", "${b}", "${c}"]`
54
+ );
55
+ fs.writeFileSync(outputFile, json, 'utf-8');
56
+ console.log(`Reduced FA written to: ${outputFile}`);
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * update-fa.js — CLI tool to apply an FA update file to a source FA JSON file.
6
+ *
7
+ * Invoked via npm scripts:
8
+ * npm run update-full-fa -- --source=<path> --update=<path> [--result=<path>]
9
+ * npm run update-compact-fa -- --source=<path> --update=<path> [--result=<path>]
10
+ *
11
+ * If --result is omitted, the output file is named <source-basename>-updated.json.
12
+ *
13
+ * Requires the library to be built first: npm run build
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { updateFullFA, updateCompactFA } = require('../lib/sfsm');
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Parse args
22
+ // ---------------------------------------------------------------------------
23
+
24
+ const args = {};
25
+ for (const arg of process.argv.slice(2)) {
26
+ const match = arg.match(/^--([^=]+)=(.+)$/);
27
+ if (match) args[match[1]] = match[2];
28
+ }
29
+
30
+ const format = args['format'];
31
+ if (!format || !['full', 'compact'].includes(format)) {
32
+ console.error('Error: --format=full|compact is required (set by the npm script).');
33
+ process.exit(1);
34
+ }
35
+
36
+ if (!args['source']) {
37
+ console.error('Error: --source=<path> is required.');
38
+ console.error(`Usage: npm run update-${format}-fa -- --source=<path> --update=<path> [--result=<path>]`);
39
+ process.exit(1);
40
+ }
41
+
42
+ if (!args['update']) {
43
+ console.error('Error: --update=<path> is required.');
44
+ console.error(`Usage: npm run update-${format}-fa -- --source=<path> --update=<path> [--result=<path>]`);
45
+ process.exit(1);
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Resolve paths
50
+ // ---------------------------------------------------------------------------
51
+
52
+ const sourceFile = path.resolve(process.cwd(), args['source']);
53
+ const updateFile = path.resolve(process.cwd(), args['update']);
54
+
55
+ let resultFile;
56
+ if (args['result']) {
57
+ resultFile = path.resolve(process.cwd(), args['result']);
58
+ } else {
59
+ const ext = path.extname(sourceFile);
60
+ const base = path.basename(sourceFile, ext);
61
+ const dir = path.dirname(sourceFile);
62
+ resultFile = path.join(dir, `${base}-updated${ext}`);
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Load & validate
67
+ // ---------------------------------------------------------------------------
68
+
69
+ if (!fs.existsSync(sourceFile)) {
70
+ console.error(`Error: Source file not found: ${sourceFile}`);
71
+ process.exit(1);
72
+ }
73
+
74
+ if (!fs.existsSync(updateFile)) {
75
+ console.error(`Error: Update file not found: ${updateFile}`);
76
+ process.exit(1);
77
+ }
78
+
79
+ let source, update;
80
+ try {
81
+ source = JSON.parse(fs.readFileSync(sourceFile, 'utf-8'));
82
+ } catch (err) {
83
+ console.error(`Error: Failed to parse source JSON: ${err.message}`);
84
+ process.exit(1);
85
+ }
86
+
87
+ try {
88
+ update = JSON.parse(fs.readFileSync(updateFile, 'utf-8'));
89
+ } catch (err) {
90
+ console.error(`Error: Failed to parse update JSON: ${err.message}`);
91
+ process.exit(1);
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Apply update
96
+ // ---------------------------------------------------------------------------
97
+
98
+ let result;
99
+ try {
100
+ result = format === 'full'
101
+ ? updateFullFA(source, update)
102
+ : updateCompactFA(source, update);
103
+ } catch (err) {
104
+ console.error(`Error: Update failed: ${err.message}`);
105
+ process.exit(1);
106
+ }
107
+
108
+ fs.writeFileSync(resultFile, JSON.stringify(result, null, 4), 'utf-8');
109
+ console.log(`Updated FA written to: ${resultFile}`);