create-zass-project 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +86 -2
- package/bin/create-zass.js +38 -0
- package/package.json +43 -4
- package/src/bootstrap.js +145 -0
- package/src/errors.js +15 -0
- package/src/options.js +105 -0
- package/src/prompts.js +42 -0
- package/vendor/bootstrap-core/src/catalog.js +81 -0
- package/vendor/bootstrap-core/src/index.js +9 -0
- package/vendor/bootstrap-core/src/plan.js +64 -0
- package/vendor/bootstrap-core/src/readme.js +60 -0
- package/vendor/bootstrap-core/src/templates.js +12 -0
- package/vendor/bootstrap-core/src/validation.js +0 -0
- package/vendor/bootstrap-core/src/verify.js +60 -0
- package/vendor/bootstrap-core/templates/zass/en.md +1443 -0
- package/vendor/bootstrap-core/templates/zass/my.md +1438 -0
- package/vendor/bootstrap-core/templates/zasselection/en.md +574 -0
- package/vendor/bootstrap-core/templates/zasselection/my.md +666 -0
- package/vendor/bootstrap-core/templates/zassimple/en.md +475 -0
- package/vendor/bootstrap-core/templates/zassimple/my.md +472 -0
- package/vendor/bootstrap-core/templates/zasspill/en.md +2184 -0
- package/vendor/bootstrap-core/templates/zasspill/my.md +2233 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dzuddiyn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,87 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-zass-project
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Bootstrap a new local ZASS project from one explicitly selected official method and language.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js 20 or newer
|
|
8
|
+
- npm
|
|
9
|
+
|
|
10
|
+
## Quick start
|
|
11
|
+
|
|
12
|
+
Interactive:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm create zass-project@latest my-project
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Non-interactive:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm create zass-project@latest my-project -- --method zassimple --lang en
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Supported methods:
|
|
25
|
+
|
|
26
|
+
- `zasspill`
|
|
27
|
+
- `zasselection`
|
|
28
|
+
- `zassimple`
|
|
29
|
+
- `zass` — Full ZASS
|
|
30
|
+
|
|
31
|
+
Supported languages:
|
|
32
|
+
|
|
33
|
+
- `en` — English
|
|
34
|
+
- `my` — Bahasa Melayu
|
|
35
|
+
|
|
36
|
+
No method or language is silently selected. In a non-interactive environment, both `--method` and `--lang` are required.
|
|
37
|
+
|
|
38
|
+
## What it creates
|
|
39
|
+
|
|
40
|
+
A successful bootstrap creates exactly:
|
|
41
|
+
|
|
42
|
+
- one selected ZASS method file;
|
|
43
|
+
- `README.md`;
|
|
44
|
+
- `.gitignore`.
|
|
45
|
+
|
|
46
|
+
For Full ZASS, the project authority file is always `ZASS.md`, including when Bahasa Melayu is selected.
|
|
47
|
+
|
|
48
|
+
## Safety and side effects
|
|
49
|
+
|
|
50
|
+
v0.1 is create-new-only. If the target already exists, `create-zass-project` refuses to overwrite or merge it.
|
|
51
|
+
|
|
52
|
+
It does **not**:
|
|
53
|
+
|
|
54
|
+
- initialize Git;
|
|
55
|
+
- create or connect a GitHub repository;
|
|
56
|
+
- register CrossAI;
|
|
57
|
+
- call Google Drive or an AI API;
|
|
58
|
+
- send telemetry;
|
|
59
|
+
- install dependencies inside the generated project;
|
|
60
|
+
- download ZASS method templates at runtime.
|
|
61
|
+
|
|
62
|
+
Generated `.gitignore` includes a baseline for common secret files. Do not put passwords, API keys, tokens, or sensitive personal data into tracked ZASS Markdown files.
|
|
63
|
+
|
|
64
|
+
## Package architecture
|
|
65
|
+
|
|
66
|
+
Project semantics come from the repository's frozen private Bootstrap Core. The npm package carries a byte-for-byte vendored runtime snapshot under `vendor/bootstrap-core/`, and repository tests enforce synchronization with canonical Core `src/` and `templates/`.
|
|
67
|
+
|
|
68
|
+
## Development
|
|
69
|
+
|
|
70
|
+
Public npm package candidate: `create-zass-project@0.1.0`.
|
|
71
|
+
|
|
72
|
+
The repository implementation directory remains `create-zass/`.
|
|
73
|
+
|
|
74
|
+
From this repository:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
node create-zass/bin/create-zass.js my-project --method zassimple --lang en
|
|
78
|
+
npm --prefix create-zass test
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Issues
|
|
82
|
+
|
|
83
|
+
Report issues in the ZASS repository issue tracker.
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { bootstrapProject, formatSuccess } from '../src/bootstrap.js';
|
|
4
|
+
import { UsageError } from '../src/errors.js';
|
|
5
|
+
import { parseArgs, resolveSelections } from '../src/options.js';
|
|
6
|
+
import { defaultPrompt } from '../src/prompts.js';
|
|
7
|
+
|
|
8
|
+
const USAGE = [
|
|
9
|
+
'Usage:',
|
|
10
|
+
' create-zass-project <project> [--method <zasspill|zasselection|zassimple|zass>] [--lang <en|my>]'
|
|
11
|
+
].join('\n');
|
|
12
|
+
|
|
13
|
+
async function main() {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
16
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
17
|
+
const resolved = await resolveSelections(parsed, {
|
|
18
|
+
interactive,
|
|
19
|
+
prompt: (kind, choices) => defaultPrompt(kind, choices)
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const report = await bootstrapProject(resolved);
|
|
23
|
+
console.log(formatSuccess(report));
|
|
24
|
+
process.exitCode = 0;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
const refusal = error.exitCode === 1;
|
|
27
|
+
console.error('CREATE ZASS');
|
|
28
|
+
console.error('');
|
|
29
|
+
console.error(`${refusal ? 'REFUSED' : 'ERROR'} — ${error.message}`);
|
|
30
|
+
if (error instanceof UsageError) {
|
|
31
|
+
console.error('');
|
|
32
|
+
console.error(USAGE);
|
|
33
|
+
}
|
|
34
|
+
process.exitCode = error.exitCode ?? 2;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-zass-project",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Bootstrap a local ZASS project with an explicitly selected official method and language",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-zass-project": "bin/create-zass.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"vendor",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test test/*.test.js",
|
|
18
|
+
"prepublishOnly": "npm test"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"zass",
|
|
25
|
+
"cli",
|
|
26
|
+
"project-bootstrap",
|
|
27
|
+
"architecture",
|
|
28
|
+
"decision-making",
|
|
29
|
+
"ai"
|
|
30
|
+
],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/dzuddiyn/ZASS-Zero-to-Architecture-Structured-Sprint.git",
|
|
34
|
+
"directory": "create-zass"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/dzuddiyn/ZASS-Zero-to-Architecture-Structured-Sprint/tree/main/create-zass#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/dzuddiyn/ZASS-Zero-to-Architecture-Structured-Sprint/issues"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"registry": "https://registry.npmjs.org/",
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"license": "MIT"
|
|
45
|
+
}
|
package/src/bootstrap.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import {
|
|
5
|
+
buildBootstrapPlan,
|
|
6
|
+
getBootstrapDescriptor,
|
|
7
|
+
verifyBootstrapSnapshot
|
|
8
|
+
} from '../vendor/bootstrap-core/src/index.js';
|
|
9
|
+
import { BootstrapRefusalError } from './errors.js';
|
|
10
|
+
|
|
11
|
+
async function pathExists(target) {
|
|
12
|
+
try {
|
|
13
|
+
await fs.lstat(target);
|
|
14
|
+
return true;
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error.code === 'ENOENT') return false;
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function readBootstrapSnapshot(projectDir) {
|
|
22
|
+
const entries = await fs.readdir(projectDir);
|
|
23
|
+
const files = [];
|
|
24
|
+
|
|
25
|
+
for (const name of entries) {
|
|
26
|
+
const fullPath = path.join(projectDir, name);
|
|
27
|
+
const stat = await fs.stat(fullPath);
|
|
28
|
+
if (!stat.isFile()) {
|
|
29
|
+
files.push({ path: name, content: null });
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
files.push({
|
|
34
|
+
path: name,
|
|
35
|
+
content: await fs.readFile(fullPath, 'utf8')
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { files };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function bootstrapProject(
|
|
43
|
+
{ target, method, language },
|
|
44
|
+
dependencies = {}
|
|
45
|
+
) {
|
|
46
|
+
const projectDir = path.resolve(target);
|
|
47
|
+
const parentDir = path.dirname(projectDir);
|
|
48
|
+
const projectName = path.basename(projectDir);
|
|
49
|
+
const writeFile = dependencies.writeFile ?? fs.writeFile;
|
|
50
|
+
const planBuilder = dependencies.buildPlan ?? buildBootstrapPlan;
|
|
51
|
+
|
|
52
|
+
if (await pathExists(projectDir)) {
|
|
53
|
+
throw new BootstrapRefusalError(`Target already exists: ${projectDir}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let parentStat;
|
|
57
|
+
try {
|
|
58
|
+
parentStat = await fs.stat(parentDir);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error.code === 'ENOENT') {
|
|
61
|
+
throw new BootstrapRefusalError(`Parent directory does not exist: ${parentDir}`);
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!parentStat.isDirectory()) {
|
|
67
|
+
throw new BootstrapRefusalError(`Parent path is not a directory: ${parentDir}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await fs.access(parentDir, constants.W_OK);
|
|
72
|
+
} catch {
|
|
73
|
+
throw new BootstrapRefusalError(`Parent directory is not writable: ${parentDir}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const plan = await planBuilder({ projectName, method, language });
|
|
77
|
+
const descriptor = getBootstrapDescriptor(method, language);
|
|
78
|
+
let created = false;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
try {
|
|
82
|
+
await fs.mkdir(projectDir);
|
|
83
|
+
created = true;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error.code === 'EEXIST') {
|
|
86
|
+
throw new BootstrapRefusalError(`Target already exists: ${projectDir}`);
|
|
87
|
+
}
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const file of plan.files) {
|
|
92
|
+
await writeFile(path.join(projectDir, file.path), file.content, 'utf8');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const snapshot = await readBootstrapSnapshot(projectDir);
|
|
96
|
+
const verification = verifyBootstrapSnapshot(plan, snapshot);
|
|
97
|
+
|
|
98
|
+
if (!verification.ok) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Materialized bootstrap verification failed: ${verification.errors
|
|
101
|
+
.map((entry) => entry.message)
|
|
102
|
+
.join('; ')}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
ok: true,
|
|
108
|
+
projectName,
|
|
109
|
+
projectDir,
|
|
110
|
+
target,
|
|
111
|
+
method,
|
|
112
|
+
language,
|
|
113
|
+
methodFile: plan.project.methodFile,
|
|
114
|
+
methodLabel: descriptor.methodLabel,
|
|
115
|
+
languageLabel: descriptor.languageLabel,
|
|
116
|
+
createdFiles: plan.files.map((file) => file.path)
|
|
117
|
+
};
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (created) {
|
|
120
|
+
await fs.rm(projectDir, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function formatSuccess(report) {
|
|
127
|
+
return [
|
|
128
|
+
'ZASS project created.',
|
|
129
|
+
'',
|
|
130
|
+
`Project: ${report.projectName}`,
|
|
131
|
+
`Method: ${report.methodLabel}`,
|
|
132
|
+
`Language: ${report.languageLabel}`,
|
|
133
|
+
'',
|
|
134
|
+
'Created:',
|
|
135
|
+
...report.createdFiles.map((name) => ` ${name}`),
|
|
136
|
+
'',
|
|
137
|
+
'Git: not initialized',
|
|
138
|
+
'GitHub: not connected',
|
|
139
|
+
'CrossAI: not registered',
|
|
140
|
+
'',
|
|
141
|
+
'Next:',
|
|
142
|
+
` cd "${report.target}"`,
|
|
143
|
+
` open ${report.methodFile} with your AI`
|
|
144
|
+
].join('\n');
|
|
145
|
+
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class UsageError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'UsageError';
|
|
5
|
+
this.exitCode = 2;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class BootstrapRefusalError extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'BootstrapRefusalError';
|
|
13
|
+
this.exitCode = 1;
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/options.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { UsageError } from './errors.js';
|
|
2
|
+
import {
|
|
3
|
+
LANGUAGE_CHOICES,
|
|
4
|
+
METHOD_CHOICES
|
|
5
|
+
} from '../vendor/bootstrap-core/src/index.js';
|
|
6
|
+
import {
|
|
7
|
+
isSupportedLanguage,
|
|
8
|
+
isSupportedMethod
|
|
9
|
+
} from '../vendor/bootstrap-core/src/catalog.js';
|
|
10
|
+
|
|
11
|
+
export { LANGUAGE_CHOICES, METHOD_CHOICES };
|
|
12
|
+
|
|
13
|
+
export function parseArgs(argv) {
|
|
14
|
+
let target;
|
|
15
|
+
let method;
|
|
16
|
+
let language;
|
|
17
|
+
|
|
18
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
19
|
+
const value = argv[index];
|
|
20
|
+
|
|
21
|
+
if (value === '--method' || value === '--lang') {
|
|
22
|
+
const next = argv[index + 1];
|
|
23
|
+
if (!next || next.startsWith('-')) {
|
|
24
|
+
throw new UsageError(`Missing value for ${value}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (value === '--method') {
|
|
28
|
+
if (method !== undefined) throw new UsageError('Duplicate --method');
|
|
29
|
+
method = next;
|
|
30
|
+
} else {
|
|
31
|
+
if (language !== undefined) throw new UsageError('Duplicate --lang');
|
|
32
|
+
language = next;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
index += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (value.startsWith('-')) {
|
|
40
|
+
throw new UsageError(`Unsupported option: ${value}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (target !== undefined) {
|
|
44
|
+
throw new UsageError('Exactly one project target is required');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
target = value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!target) {
|
|
51
|
+
throw new UsageError('Project target is required');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { target, method, language };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function validateMethod(method) {
|
|
58
|
+
if (!isSupportedMethod(method)) {
|
|
59
|
+
throw new UsageError(
|
|
60
|
+
`Invalid method: ${method}. Expected one of: ${METHOD_CHOICES.map((choice) => choice.value).join(', ')}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validateLanguage(language) {
|
|
66
|
+
if (!isSupportedLanguage(language)) {
|
|
67
|
+
throw new UsageError(
|
|
68
|
+
`Invalid language: ${language}. Expected one of: ${LANGUAGE_CHOICES.map((choice) => choice.value).join(', ')}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function resolveSelections(
|
|
74
|
+
parsed,
|
|
75
|
+
{ interactive = false, prompt } = {}
|
|
76
|
+
) {
|
|
77
|
+
let { method, language } = parsed;
|
|
78
|
+
|
|
79
|
+
if (method !== undefined) validateMethod(method);
|
|
80
|
+
if (language !== undefined) validateLanguage(language);
|
|
81
|
+
|
|
82
|
+
if (method === undefined) {
|
|
83
|
+
if (!interactive) {
|
|
84
|
+
throw new UsageError('Method required in non-interactive mode; use --method');
|
|
85
|
+
}
|
|
86
|
+
if (typeof prompt !== 'function') {
|
|
87
|
+
throw new UsageError('Interactive method prompt is unavailable');
|
|
88
|
+
}
|
|
89
|
+
method = await prompt('method', METHOD_CHOICES);
|
|
90
|
+
validateMethod(method);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (language === undefined) {
|
|
94
|
+
if (!interactive) {
|
|
95
|
+
throw new UsageError('Language required in non-interactive mode; use --lang');
|
|
96
|
+
}
|
|
97
|
+
if (typeof prompt !== 'function') {
|
|
98
|
+
throw new UsageError('Interactive language prompt is unavailable');
|
|
99
|
+
}
|
|
100
|
+
language = await prompt('language', LANGUAGE_CHOICES);
|
|
101
|
+
validateLanguage(language);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { ...parsed, method, language };
|
|
105
|
+
}
|
package/src/prompts.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
|
|
3
|
+
export async function promptChoice(question, choices, io = {}) {
|
|
4
|
+
const input = io.input ?? process.stdin;
|
|
5
|
+
const output = io.output ?? process.stdout;
|
|
6
|
+
const rl = createInterface({ input, output });
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
while (true) {
|
|
10
|
+
output.write(`${question}\n\n`);
|
|
11
|
+
for (let index = 0; index < choices.length; index += 1) {
|
|
12
|
+
const choice = choices[index];
|
|
13
|
+
const description = choice.description ? ` — ${choice.description}` : '';
|
|
14
|
+
output.write(` ${index + 1}. ${choice.label}${description}\n`);
|
|
15
|
+
}
|
|
16
|
+
output.write('\n');
|
|
17
|
+
|
|
18
|
+
const answer = (await rl.question(`Select [1-${choices.length}]: `)).trim();
|
|
19
|
+
const selected = Number(answer);
|
|
20
|
+
|
|
21
|
+
if (
|
|
22
|
+
Number.isInteger(selected) &&
|
|
23
|
+
selected >= 1 &&
|
|
24
|
+
selected <= choices.length
|
|
25
|
+
) {
|
|
26
|
+
return choices[selected - 1].value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
output.write(`Please choose a number from 1 to ${choices.length}.\n\n`);
|
|
30
|
+
}
|
|
31
|
+
} finally {
|
|
32
|
+
rl.close();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function defaultPrompt(kind, choices, io) {
|
|
37
|
+
const question =
|
|
38
|
+
kind === 'method'
|
|
39
|
+
? 'Which ZASS method do you want to start with?'
|
|
40
|
+
: 'Choose language:';
|
|
41
|
+
return promptChoice(question, choices, io);
|
|
42
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export const CORE_CONTRACT_VERSION = '0.1';
|
|
2
|
+
|
|
3
|
+
export const METHOD_CHOICES = Object.freeze([
|
|
4
|
+
Object.freeze({ value: 'zasspill', label: 'ZASSPILL', description: 'capture / continuity' }),
|
|
5
|
+
Object.freeze({ value: 'zasselection', label: 'ZASSELECTION', description: 'compare / decide' }),
|
|
6
|
+
Object.freeze({ value: 'zassimple', label: 'ZASSIMPLE', description: 'design / execute' }),
|
|
7
|
+
Object.freeze({ value: 'zass', label: 'FULL ZASS', description: 'governed architecture' })
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
export const LANGUAGE_CHOICES = Object.freeze([
|
|
11
|
+
Object.freeze({ value: 'en', label: 'English' }),
|
|
12
|
+
Object.freeze({ value: 'my', label: 'Bahasa Melayu' })
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const METHODS = Object.freeze({
|
|
16
|
+
zasspill: Object.freeze({
|
|
17
|
+
value: 'zasspill',
|
|
18
|
+
label: 'ZASSPILL',
|
|
19
|
+
templates: Object.freeze({ en: 'zasspill/en.md', my: 'zasspill/my.md' }),
|
|
20
|
+
outputs: Object.freeze({ en: 'ZASSPILL_EN.md', my: 'ZASSPILL_MY.md' })
|
|
21
|
+
}),
|
|
22
|
+
zasselection: Object.freeze({
|
|
23
|
+
value: 'zasselection',
|
|
24
|
+
label: 'ZASSELECTION',
|
|
25
|
+
templates: Object.freeze({ en: 'zasselection/en.md', my: 'zasselection/my.md' }),
|
|
26
|
+
outputs: Object.freeze({ en: 'ZASSELECTION_EN.md', my: 'ZASSELECTION_MY.md' })
|
|
27
|
+
}),
|
|
28
|
+
zassimple: Object.freeze({
|
|
29
|
+
value: 'zassimple',
|
|
30
|
+
label: 'ZASSIMPLE',
|
|
31
|
+
templates: Object.freeze({ en: 'zassimple/en.md', my: 'zassimple/my.md' }),
|
|
32
|
+
outputs: Object.freeze({ en: 'ZASSIMPLE_EN.md', my: 'ZASSIMPLE_MY.md' })
|
|
33
|
+
}),
|
|
34
|
+
zass: Object.freeze({
|
|
35
|
+
value: 'zass',
|
|
36
|
+
label: 'FULL ZASS',
|
|
37
|
+
templates: Object.freeze({ en: 'zass/en.md', my: 'zass/my.md' }),
|
|
38
|
+
outputs: Object.freeze({ en: 'ZASS.md', my: 'ZASS.md' })
|
|
39
|
+
})
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const LANGUAGES = Object.freeze({
|
|
43
|
+
en: Object.freeze({ value: 'en', label: 'English' }),
|
|
44
|
+
my: Object.freeze({ value: 'my', label: 'Bahasa Melayu' })
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export function isSupportedMethod(method) {
|
|
48
|
+
return Object.hasOwn(METHODS, method);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isSupportedLanguage(language) {
|
|
52
|
+
return Object.hasOwn(LANGUAGES, language);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getMethodDescriptor(method) {
|
|
56
|
+
return METHODS[method] ?? null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getLanguageDescriptor(language) {
|
|
60
|
+
return LANGUAGES[language] ?? null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getBootstrapDescriptor(method, language) {
|
|
64
|
+
const methodDescriptor = getMethodDescriptor(method);
|
|
65
|
+
const languageDescriptor = getLanguageDescriptor(language);
|
|
66
|
+
const template = methodDescriptor?.templates?.[language];
|
|
67
|
+
const methodFile = methodDescriptor?.outputs?.[language];
|
|
68
|
+
|
|
69
|
+
if (!methodDescriptor || !languageDescriptor || !template || !methodFile) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return Object.freeze({
|
|
74
|
+
method,
|
|
75
|
+
language,
|
|
76
|
+
methodLabel: methodDescriptor.label,
|
|
77
|
+
languageLabel: languageDescriptor.label,
|
|
78
|
+
template,
|
|
79
|
+
methodFile
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { CORE_CONTRACT_VERSION, getBootstrapDescriptor } from './catalog.js';
|
|
2
|
+
import { buildProjectReadme, GITIGNORE_CONTENT } from './readme.js';
|
|
3
|
+
import { loadBootstrapTemplate } from './templates.js';
|
|
4
|
+
import { validateBootstrapInput, validateBootstrapPlan } from './validation.js';
|
|
5
|
+
|
|
6
|
+
export async function buildBootstrapPlan({ projectName, method, language }) {
|
|
7
|
+
const input = validateBootstrapInput({ projectName, method, language });
|
|
8
|
+
if (!input.ok) {
|
|
9
|
+
const error = new Error(input.errors.map((entry) => entry.message).join('; '));
|
|
10
|
+
error.name = 'BootstrapCoreInputError';
|
|
11
|
+
error.errors = input.errors;
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const descriptor = getBootstrapDescriptor(method, language);
|
|
16
|
+
const methodContent = await loadBootstrapTemplate(method, language);
|
|
17
|
+
const readmeContent = buildProjectReadme({
|
|
18
|
+
projectName,
|
|
19
|
+
methodLabel: descriptor.methodLabel,
|
|
20
|
+
methodFile: descriptor.methodFile,
|
|
21
|
+
language
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const plan = {
|
|
25
|
+
contractVersion: CORE_CONTRACT_VERSION,
|
|
26
|
+
project: {
|
|
27
|
+
name: projectName,
|
|
28
|
+
method,
|
|
29
|
+
language,
|
|
30
|
+
methodFile: descriptor.methodFile
|
|
31
|
+
},
|
|
32
|
+
files: [
|
|
33
|
+
{
|
|
34
|
+
path: descriptor.methodFile,
|
|
35
|
+
role: 'method',
|
|
36
|
+
content: methodContent
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
path: 'README.md',
|
|
40
|
+
role: 'readme',
|
|
41
|
+
content: readmeContent
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
path: '.gitignore',
|
|
45
|
+
role: 'gitignore',
|
|
46
|
+
content: GITIGNORE_CONTENT
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const validation = validateBootstrapPlan(plan);
|
|
52
|
+
if (!validation.ok) {
|
|
53
|
+
const error = new Error(
|
|
54
|
+
`Generated bootstrap plan failed validation: ${validation.errors
|
|
55
|
+
.map((entry) => entry.message)
|
|
56
|
+
.join('; ')}`
|
|
57
|
+
);
|
|
58
|
+
error.name = 'BootstrapCorePlanError';
|
|
59
|
+
error.errors = validation.errors;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return plan;
|
|
64
|
+
}
|