tux-review 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/README.md +67 -0
- package/bin/tux.js +17 -0
- package/client/tux-review.css +155 -0
- package/client/tux-review.js +586 -0
- package/package.json +59 -0
- package/skills/tux-design-create.md +50 -0
- package/skills/tux-design-incorporate.md +61 -0
- package/skills/tux-design-install.md +56 -0
- package/skills/tux-design-start-review.md +51 -0
- package/skills/tux-design-stop-review.md +44 -0
- package/skills/tux-feedback-delete.md +45 -0
- package/skills/tux-feedback-export.md +47 -0
- package/skills/tux-feedback-show.md +55 -0
- package/skills/tux-live-create.md +50 -0
- package/skills/tux-live-incorporate.md +62 -0
- package/skills/tux-live-install.md +63 -0
- package/skills/tux-live-start-review.md +47 -0
- package/skills/tux-live-stop-review.md +46 -0
- package/src/canonical.js +28 -0
- package/src/cli.js +229 -0
- package/src/config.js +72 -0
- package/src/design.js +140 -0
- package/src/errors.js +28 -0
- package/src/feedback.js +289 -0
- package/src/ids.js +57 -0
- package/src/index.js +17 -0
- package/src/live.js +261 -0
- package/src/schema.js +77 -0
- package/src/serve-main.js +42 -0
- package/src/server.js +347 -0
- package/src/store.js +72 -0
- package/src/templates.js +44 -0
- package/templates/angular/README.md +19 -0
- package/templates/angular/angular.json +49 -0
- package/templates/angular/package.json +25 -0
- package/templates/angular/src/app/app.component.html +40 -0
- package/templates/angular/src/app/app.component.ts +30 -0
- package/templates/angular/src/index.html +11 -0
- package/templates/angular/src/main.ts +4 -0
- package/templates/angular/src/styles.css +11 -0
- package/templates/angular/tsconfig.app.json +8 -0
- package/templates/angular/tsconfig.json +11 -0
- package/templates/react/README.md +18 -0
- package/templates/react/index.html +12 -0
- package/templates/react/package.json +18 -0
- package/templates/react/src/App.jsx +80 -0
- package/templates/react/src/main.jsx +6 -0
- package/templates/react/src/styles.css +11 -0
- package/templates/vanilla/README.md +22 -0
- package/templates/vanilla/index.html +18 -0
- package/templates/vanilla/package.json +6 -0
- package/templates/vanilla/src/app.js +79 -0
- package/templates/vanilla/src/styles.css +11 -0
- package/templates/vue/README.md +18 -0
- package/templates/vue/index.html +12 -0
- package/templates/vue/package.json +17 -0
- package/templates/vue/src/App.vue +61 -0
- package/templates/vue/src/main.js +5 -0
- package/templates/vue/src/styles.css +11 -0
- package/templates/vue/vite.config.js +6 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tux-live-stop-review
|
|
3
|
+
description: Stop the live review proxy cleanly — check state first, report the session summary, preserve every feedback item.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# tux-live-stop-review
|
|
7
|
+
|
|
8
|
+
End a live review session deliberately: check the proxy state, hand over
|
|
9
|
+
a session summary, then stop. In proxy mode the application itself was
|
|
10
|
+
never modified and keeps serving untouched after the proxy is gone; in
|
|
11
|
+
spawn mode the spawned review application is shut down with the session
|
|
12
|
+
(SPC section 41).
|
|
13
|
+
|
|
14
|
+
## Resolve the CLI
|
|
15
|
+
|
|
16
|
+
Choose one TUX CLI variant; both implement the same commands and canonical
|
|
17
|
+
JSON output, so do not install both:
|
|
18
|
+
|
|
19
|
+
1. **Node.js/npm** — use an existing `tux --version` on PATH, or run
|
|
20
|
+
`npx --yes --package=tux-review tux --version` and prefix commands with
|
|
21
|
+
`npx --yes --package=tux-review tux …`.
|
|
22
|
+
2. **Python/PyPI** — install with `pipx install tux-review` (or
|
|
23
|
+
`python -m pip install tux-review` in the active virtual environment),
|
|
24
|
+
then use `tux --version`. For a one-off run, use
|
|
25
|
+
`pipx run tux-review tux --version`.
|
|
26
|
+
|
|
27
|
+
All commands below use the neutral form `tux <domain> <action>`. Replace
|
|
28
|
+
`tux` with the Node `npx … tux` prefix only when you chose the Node
|
|
29
|
+
one-off variant.
|
|
30
|
+
|
|
31
|
+
## Workflow
|
|
32
|
+
|
|
33
|
+
1. Check the current state: `tux live status --format json` — reports
|
|
34
|
+
`running` with URL, session and feedback count, or `stopped`. Never
|
|
35
|
+
stop blind.
|
|
36
|
+
2. Report the session summary before stopping: target URL, session name,
|
|
37
|
+
feedback count.
|
|
38
|
+
3. Stop with `tux live stop-review --format json`. The output confirms
|
|
39
|
+
`stopped: true` with the pid; `stopped: false` with reason
|
|
40
|
+
`not running` means there was nothing to stop. In spawn mode the
|
|
41
|
+
spawned application process is stopped together with the proxy.
|
|
42
|
+
4. Verify: `tux live status` now reports `stopped`, and the feedback
|
|
43
|
+
store still contains every item — stopping MUST NOT delete feedback
|
|
44
|
+
(SPC section 41).
|
|
45
|
+
5. Report the final state: review stopped, feedback count preserved,
|
|
46
|
+
store path.
|
package/src/canonical.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical JSON serialization.
|
|
3
|
+
*
|
|
4
|
+
* Canonical JSON is UTF-8, 2-space indented, `\n` line endings, keys in
|
|
5
|
+
* the fixed schema order (objects are built in that order by the
|
|
6
|
+
* constructors; free-form `ui_state` keys are sorted alphabetically) with
|
|
7
|
+
* a single trailing newline when written as a document. Both reference
|
|
8
|
+
* implementations (Node, Python) must emit byte-identical bytes.
|
|
9
|
+
*
|
|
10
|
+
* @module canonical
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Serialize a value as canonical JSON text (no trailing newline). */
|
|
14
|
+
export function canonicalJson(value) {
|
|
15
|
+
return JSON.stringify(value, null, 2);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Serialize a value as a compact single-line JSON text (no newline). */
|
|
19
|
+
export function compactJson(value) {
|
|
20
|
+
return JSON.stringify(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Sort the keys of a free-form object alphabetically (one level). */
|
|
24
|
+
export function sortedObject(obj) {
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const k of Object.keys(obj).sort()) out[k] = obj[k];
|
|
27
|
+
return out;
|
|
28
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tux` command-line interface (SPC sections 25, 74, 75).
|
|
3
|
+
*
|
|
4
|
+
* Grammar: `tux <domain> <action> [arguments] [options]`.
|
|
5
|
+
* Machine output: canonical JSON on stdout; diagnostics on stderr as
|
|
6
|
+
* `error: <message>` lines with canonical exit codes.
|
|
7
|
+
*
|
|
8
|
+
* @module cli
|
|
9
|
+
*/
|
|
10
|
+
import { CliError, EXIT } from './errors.js';
|
|
11
|
+
import { loadConfig } from './config.js';
|
|
12
|
+
import { opShow, opCreate, opUpdate, opDelete, opClear, opExport, opIncorporate, opValidate } from './feedback.js';
|
|
13
|
+
import { opDesignInstall, opDesignCreate, opLiveCreate, opDesignStart, opDesignStatus, opDesignStop } from './design.js';
|
|
14
|
+
import { opLiveInstall, opLiveStart, opLiveStatus, opLiveStop } from './live.js';
|
|
15
|
+
|
|
16
|
+
export const VERSION = '0.1.0';
|
|
17
|
+
|
|
18
|
+
const VALUE_FLAGS = new Set([
|
|
19
|
+
'config', 'format', 'type', 'text', 'route', 'page', 'component', 'component-instance', 'instance',
|
|
20
|
+
'tux-id', 'test-id', 'session', 'environment', 'origin', 'status', 'strategy', 'result', 'note', 'record',
|
|
21
|
+
'url', 'target-port', 'port', 'host', 'store', 'project-id', 'name', 'framework', 'dir', 'root', 'out',
|
|
22
|
+
'mode', 'target',
|
|
23
|
+
]);
|
|
24
|
+
const BOOL_FLAGS = new Set(['mine', 'all', 'force', 'strict', 'dry-run', 'no-interactive', 'foreground', 'help', 'version']);
|
|
25
|
+
|
|
26
|
+
export function parseArgs(args) {
|
|
27
|
+
const flags = {};
|
|
28
|
+
const positional = [];
|
|
29
|
+
let i = 0;
|
|
30
|
+
while (i < args.length) {
|
|
31
|
+
const a = args[i];
|
|
32
|
+
if (a === '--') {
|
|
33
|
+
positional.push(...args.slice(i + 1));
|
|
34
|
+
return { flags, positional, cmd: null };
|
|
35
|
+
}
|
|
36
|
+
if (a.startsWith('--')) {
|
|
37
|
+
const name = a.slice(2);
|
|
38
|
+
if (VALUE_FLAGS.has(name)) {
|
|
39
|
+
const v = args[i + 1];
|
|
40
|
+
if (v === undefined) throw new CliError(EXIT.usage, `option --${name} requires a value`);
|
|
41
|
+
flags[name] = v;
|
|
42
|
+
i += 2;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (BOOL_FLAGS.has(name)) {
|
|
46
|
+
flags[name] = true;
|
|
47
|
+
i += 1;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
throw new CliError(EXIT.usage, `unknown option: --${name}`);
|
|
51
|
+
}
|
|
52
|
+
positional.push(a);
|
|
53
|
+
i += 1;
|
|
54
|
+
}
|
|
55
|
+
return { flags, positional, cmd: null };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function optsFrom(cwd, config, configPath, flags) {
|
|
59
|
+
return {
|
|
60
|
+
cwd,
|
|
61
|
+
config,
|
|
62
|
+
configPath,
|
|
63
|
+
format: flags.format ?? null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function requireFormat(flags, fallback) {
|
|
68
|
+
return flags.format ?? fallback;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function run(argv, context = {}) {
|
|
72
|
+
const cwd = context.cwd ?? process.cwd();
|
|
73
|
+
let parsed;
|
|
74
|
+
try {
|
|
75
|
+
parsed = parseArgs(argv);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
if (e instanceof CliError) return fail(e);
|
|
78
|
+
throw e;
|
|
79
|
+
}
|
|
80
|
+
const { flags, positional } = parsed;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
if (flags.version) return { stdout: `tux ${VERSION}\n`, exit: EXIT.ok };
|
|
84
|
+
if (flags.help || positional.length === 0) return { stdout: help(), exit: EXIT.ok };
|
|
85
|
+
|
|
86
|
+
const domain = positional[0];
|
|
87
|
+
const action = positional[1];
|
|
88
|
+
|
|
89
|
+
if (domain === 'feedback') {
|
|
90
|
+
const { config, configPath } = loadConfig(cwd, flags);
|
|
91
|
+
const opts = optsFrom(cwd, config, configPath, flags);
|
|
92
|
+
opts.format = requireFormat(flags, opts.format);
|
|
93
|
+
switch (action) {
|
|
94
|
+
case 'show': {
|
|
95
|
+
const id = positional[2];
|
|
96
|
+
opts.format = flags.format ?? 'text';
|
|
97
|
+
return opShow(opts, id, {
|
|
98
|
+
status: flags.status, type: flags.type, mine: flags.mine ?? false,
|
|
99
|
+
route: flags.route, session: flags.session, origin: flags.origin,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
case 'create':
|
|
103
|
+
return opCreate(opts, {
|
|
104
|
+
type: flags.type ?? 'issue',
|
|
105
|
+
text: flags.text,
|
|
106
|
+
route: flags.route,
|
|
107
|
+
page: flags.page,
|
|
108
|
+
component: flags.component,
|
|
109
|
+
instance: flags['component-instance'] ?? flags.instance,
|
|
110
|
+
tuxId: flags['tux-id'],
|
|
111
|
+
testId: flags['test-id'],
|
|
112
|
+
session: flags.session,
|
|
113
|
+
environment: flags.environment,
|
|
114
|
+
origin: flags.origin,
|
|
115
|
+
});
|
|
116
|
+
case 'update': {
|
|
117
|
+
const id = positional[2];
|
|
118
|
+
if (!id) throw new CliError(EXIT.usage, 'usage: tux feedback update <feedback-id>');
|
|
119
|
+
return opUpdate(opts, id, { text: flags.text, type: flags.type, status: flags.status });
|
|
120
|
+
}
|
|
121
|
+
case 'delete': {
|
|
122
|
+
const id = positional[2];
|
|
123
|
+
if (!id) throw new CliError(EXIT.usage, 'usage: tux feedback delete <feedback-id>');
|
|
124
|
+
opts.format = flags.format ?? 'text';
|
|
125
|
+
return opDelete(opts, id);
|
|
126
|
+
}
|
|
127
|
+
case 'clear':
|
|
128
|
+
opts.format = flags.format ?? 'text';
|
|
129
|
+
return opClear(opts, flags.all ? 'all' : flags.mine ? 'mine' : undefined, {
|
|
130
|
+
force: flags.force ?? false, route: flags.route, session: flags.session,
|
|
131
|
+
});
|
|
132
|
+
case 'export':
|
|
133
|
+
return opExport(opts, flags.format === 'jsonl' ? 'jsonl' : 'json');
|
|
134
|
+
case 'incorporate':
|
|
135
|
+
return opIncorporate(opts, flags.strategy, {
|
|
136
|
+
mine: flags.mine ?? false, route: flags.route, session: flags.session, origin: flags.origin,
|
|
137
|
+
});
|
|
138
|
+
case 'validate':
|
|
139
|
+
return opValidate(opts, {
|
|
140
|
+
record: flags.record ?? undefined,
|
|
141
|
+
result: flags.result,
|
|
142
|
+
note: flags.note,
|
|
143
|
+
strict: flags.strict ?? false,
|
|
144
|
+
origin: flags.origin,
|
|
145
|
+
});
|
|
146
|
+
default:
|
|
147
|
+
throw new CliError(EXIT.usage, `unknown action: ${action ?? '(missing)'} for domain ${domain}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (domain === 'design' || domain === 'live') {
|
|
152
|
+
const { config, configPath } = loadConfig(cwd, flags);
|
|
153
|
+
const opts = optsFrom(cwd, config, configPath, flags);
|
|
154
|
+
const isDesign = domain === 'design';
|
|
155
|
+
opts.format = requireFormat(flags, 'json');
|
|
156
|
+
switch (action) {
|
|
157
|
+
case 'install':
|
|
158
|
+
return isDesign
|
|
159
|
+
? opDesignInstall(opts, { framework: flags.framework, root: flags.root })
|
|
160
|
+
: opLiveInstall(opts);
|
|
161
|
+
case 'create':
|
|
162
|
+
return isDesign
|
|
163
|
+
? opDesignCreate(opts, { framework: flags.framework, name: flags.name })
|
|
164
|
+
: opLiveCreate(opts, { framework: flags.framework, name: flags.name });
|
|
165
|
+
case 'start-review':
|
|
166
|
+
opts.format = flags.format ?? 'json';
|
|
167
|
+
if (isDesign) {
|
|
168
|
+
return await opDesignStart(opts, {
|
|
169
|
+
dir: flags.dir,
|
|
170
|
+
port: flags.port ? Number(flags.port) : undefined,
|
|
171
|
+
host: flags.host,
|
|
172
|
+
session: flags.session,
|
|
173
|
+
environment: flags.environment,
|
|
174
|
+
dryRun: flags['dry-run'] ?? false,
|
|
175
|
+
foreground: flags.foreground ?? false,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return await opLiveStart(opts, {
|
|
179
|
+
url: flags.url ?? null,
|
|
180
|
+
cmd: parsed.cmd && parsed.cmd.length ? parsed.cmd.join(' ') : (positional.length > 2 ? positional.slice(2).join(' ') : null),
|
|
181
|
+
targetPort: flags['target-port'] ? Number(flags['target-port']) : undefined,
|
|
182
|
+
port: flags.port ? Number(flags.port) : undefined,
|
|
183
|
+
host: flags.host,
|
|
184
|
+
session: flags.session,
|
|
185
|
+
environment: flags.environment,
|
|
186
|
+
dryRun: flags['dry-run'] ?? false,
|
|
187
|
+
foreground: flags.foreground ?? false,
|
|
188
|
+
});
|
|
189
|
+
case 'status': {
|
|
190
|
+
opts.format = flags.format ?? 'text';
|
|
191
|
+
return await opLiveStatus(opts);
|
|
192
|
+
}
|
|
193
|
+
case 'stop-review': {
|
|
194
|
+
opts.format = flags.format ?? 'text';
|
|
195
|
+
return opLiveStop(opts);
|
|
196
|
+
}
|
|
197
|
+
default:
|
|
198
|
+
throw new CliError(EXIT.usage, `unknown action: ${action ?? '(missing)'} for domain ${domain}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
throw new CliError(EXIT.usage, `unknown command: ${domain}`);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
if (e instanceof CliError) return fail(e);
|
|
205
|
+
return { stdout: '', stderr: `error: internal: ${e.message}\n`, exit: EXIT.general };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function fail(e) {
|
|
210
|
+
return { stdout: '', stderr: `error: ${e.message}\n`, exit: e.code };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function help() {
|
|
214
|
+
return [
|
|
215
|
+
'tux <domain> <action> [arguments] [options]',
|
|
216
|
+
'',
|
|
217
|
+
'domains:',
|
|
218
|
+
' design install | create | start-review | status | stop-review',
|
|
219
|
+
' live install | create | start-review | status | stop-review',
|
|
220
|
+
' feedback show | create | update | delete | clear | export | incorporate | validate',
|
|
221
|
+
'',
|
|
222
|
+
'options:',
|
|
223
|
+
' --config <path> project config (default: tux.config.json)',
|
|
224
|
+
' --format json|text output format',
|
|
225
|
+
' --version print version',
|
|
226
|
+
' --help this help',
|
|
227
|
+
'',
|
|
228
|
+
].join('\n');
|
|
229
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project configuration (`tux.config.json`) with canonical precedence:
|
|
3
|
+
* CLI → environment → config file → defaults (SPC section 76).
|
|
4
|
+
*
|
|
5
|
+
* @module config
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { basename, join } from 'node:path';
|
|
9
|
+
import { CliError } from './errors.js';
|
|
10
|
+
|
|
11
|
+
export const CONFIG_FILE = 'tux.config.json';
|
|
12
|
+
|
|
13
|
+
export function defaultConfig(cwd) {
|
|
14
|
+
return {
|
|
15
|
+
project_id: basename(cwd),
|
|
16
|
+
design: { root: 'requirements', framework: 'vanilla' },
|
|
17
|
+
review: { enabled: true, store: '.tux/feedback.json', host: '127.0.0.1', port: 4173 },
|
|
18
|
+
identity: { provider: 'local', user_id: 'anonymous', display_name: 'Anonymous', admins: [] },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} cwd project working directory
|
|
24
|
+
* @param {{config?: string}} cliOpts CLI options (`--config`)
|
|
25
|
+
* @returns {{config: object, configPath: string|null, cwd: string}}
|
|
26
|
+
*/
|
|
27
|
+
export function loadConfig(cwd, cliOpts = {}) {
|
|
28
|
+
const config = defaultConfig(cwd);
|
|
29
|
+
let configPath = cliOpts.config ?? process.env.TUX_CONFIG ?? null;
|
|
30
|
+
if (configPath === null && existsSync(join(cwd, CONFIG_FILE))) configPath = join(cwd, CONFIG_FILE);
|
|
31
|
+
if (configPath !== null) {
|
|
32
|
+
if (!existsSync(configPath)) throw new CliError(3, `config file not found: ${configPath}`);
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
36
|
+
} catch (e) {
|
|
37
|
+
throw new CliError(3, `invalid config JSON in ${configPath}: ${e.message}`);
|
|
38
|
+
}
|
|
39
|
+
mergeConfig(config, parsed);
|
|
40
|
+
}
|
|
41
|
+
const env = process.env;
|
|
42
|
+
if (env.TUX_PROJECT_ID !== undefined) config.project_id = env.TUX_PROJECT_ID;
|
|
43
|
+
if (env.TUX_STORE !== undefined) config.review.store = env.TUX_STORE;
|
|
44
|
+
if (env.TUX_HOST !== undefined) config.review.host = env.TUX_HOST;
|
|
45
|
+
if (env.TUX_PORT !== undefined) config.review.port = Number(env.TUX_PORT);
|
|
46
|
+
if (env.TUX_USER_ID !== undefined) config.identity.user_id = env.TUX_USER_ID;
|
|
47
|
+
if (env.TUX_DISPLAY_NAME !== undefined) config.identity.display_name = env.TUX_DISPLAY_NAME;
|
|
48
|
+
return { config, configPath, cwd };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function mergeConfig(target, patch) {
|
|
52
|
+
for (const [key, value] of Object.entries(patch ?? {})) {
|
|
53
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value) && typeof target[key] === 'object') {
|
|
54
|
+
mergeConfig(target[key], value);
|
|
55
|
+
} else {
|
|
56
|
+
target[key] = value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Resolve the effective reviewer identity. */
|
|
62
|
+
export function resolveIdentity(config) {
|
|
63
|
+
return {
|
|
64
|
+
user_id: config.identity.user_id || 'anonymous',
|
|
65
|
+
display_name: config.identity.display_name || 'Anonymous',
|
|
66
|
+
admins: config.identity.admins ?? [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function isAdmin(config, userId) {
|
|
71
|
+
return resolveIdentity(config).admins.includes(userId);
|
|
72
|
+
}
|
package/src/design.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tux design <action>` — install, create, start-review, status, stop-review (SPC sections 28–38).
|
|
3
|
+
* `tux live create` (SPC section 32) shares the create primitive with `"kind": "live"`.
|
|
4
|
+
*
|
|
5
|
+
* @module design
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { join, relative, resolve } from 'node:path';
|
|
9
|
+
import { CliError, EXIT } from './errors.js';
|
|
10
|
+
import { canonicalJson } from './canonical.js';
|
|
11
|
+
import { CONFIG_FILE, defaultConfig } from './config.js';
|
|
12
|
+
import { SUPPORTED_FRAMEWORKS, designTemplate } from './templates.js';
|
|
13
|
+
import { startServer, opLiveStatus, opLiveStop } from './live.js';
|
|
14
|
+
|
|
15
|
+
function textReport(lines) {
|
|
16
|
+
return { stdout: lines.join('\n') + '\n', exit: EXIT.ok };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function relPath(cwd, p) {
|
|
20
|
+
const rel = relative(cwd, p);
|
|
21
|
+
if (rel === '') return '.';
|
|
22
|
+
return rel.startsWith('..') ? p : rel;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function opDesignInstall(opts, spec) {
|
|
26
|
+
const cwd = opts.cwd;
|
|
27
|
+
const framework = spec.framework ?? opts.config.design.framework ?? 'vanilla';
|
|
28
|
+
if (!SUPPORTED_FRAMEWORKS.includes(framework)) {
|
|
29
|
+
throw new CliError(EXIT.usage, `unsupported framework: ${framework} (expected ${SUPPORTED_FRAMEWORKS.join(', ')})`);
|
|
30
|
+
}
|
|
31
|
+
const configPath = opts.configPath ?? join(cwd, CONFIG_FILE);
|
|
32
|
+
let config;
|
|
33
|
+
if (existsSync(configPath)) {
|
|
34
|
+
config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
35
|
+
} else {
|
|
36
|
+
config = defaultConfig(cwd);
|
|
37
|
+
}
|
|
38
|
+
config.design = config.design ?? {};
|
|
39
|
+
config.design.framework = framework;
|
|
40
|
+
if (spec.root) config.design.root = spec.root;
|
|
41
|
+
const root = config.design.root ?? 'requirements';
|
|
42
|
+
writeFileSync(configPath, canonicalJson(config) + '\n', 'utf8');
|
|
43
|
+
const report = {
|
|
44
|
+
action: 'install',
|
|
45
|
+
kind: 'design',
|
|
46
|
+
framework,
|
|
47
|
+
root,
|
|
48
|
+
config: opts.configPath ? relPath(cwd, opts.configPath) : CONFIG_FILE,
|
|
49
|
+
next: [`tux design create --framework ${framework}`, 'tux design start-review'],
|
|
50
|
+
};
|
|
51
|
+
if (opts.format === 'text') {
|
|
52
|
+
return textReport([
|
|
53
|
+
`installed design review (framework ${framework})`,
|
|
54
|
+
`config: ${report.config}`,
|
|
55
|
+
`next: ${report.next.join(' | ')}`,
|
|
56
|
+
]);
|
|
57
|
+
}
|
|
58
|
+
return { stdout: canonicalJson(report) + '\n', exit: EXIT.ok };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const DEV_PORT = { vanilla: 4173, react: 5173, vue: 5173, angular: 4200 };
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create the runnable design scaffold (`tux design create`) or the identical
|
|
65
|
+
* live-app scaffold (`tux live create`, `"kind": "live"`).
|
|
66
|
+
*/
|
|
67
|
+
export function opDesignCreate(opts, spec) {
|
|
68
|
+
const kind = spec.kind ?? 'design';
|
|
69
|
+
const cwd = opts.cwd;
|
|
70
|
+
const framework = spec.framework;
|
|
71
|
+
if (!framework) throw new CliError(EXIT.usage, '--framework is required');
|
|
72
|
+
if (!SUPPORTED_FRAMEWORKS.includes(framework)) {
|
|
73
|
+
throw new CliError(EXIT.usage, `unsupported framework: ${framework} (expected ${SUPPORTED_FRAMEWORKS.join(', ')})`);
|
|
74
|
+
}
|
|
75
|
+
if (!spec.name || !/^[a-z0-9][a-z0-9-]*$/.test(spec.name)) {
|
|
76
|
+
throw new CliError(EXIT.usage, '--name must be a lowercase slug (letters, digits, dashes)');
|
|
77
|
+
}
|
|
78
|
+
const root = opts.config.design.root ?? 'requirements';
|
|
79
|
+
const designDir = resolve(cwd, root, spec.name, 'design');
|
|
80
|
+
if (existsSync(designDir)) {
|
|
81
|
+
throw new CliError(EXIT.conflict, `design directory already exists: ${root}/${spec.name}/design`);
|
|
82
|
+
}
|
|
83
|
+
const files = designTemplate(framework);
|
|
84
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
85
|
+
const p = join(designDir, rel);
|
|
86
|
+
mkdirSync(join(p, '..'), { recursive: true });
|
|
87
|
+
writeFileSync(p, content, 'utf8');
|
|
88
|
+
}
|
|
89
|
+
const domain = kind === 'live' ? 'live' : 'design';
|
|
90
|
+
const next = kind === 'live'
|
|
91
|
+
? ['tux live install', `tux live start-review --url http://localhost:${DEV_PORT[framework]}`]
|
|
92
|
+
: ['tux design start-review', 'review in the browser'];
|
|
93
|
+
const report = {
|
|
94
|
+
action: 'create',
|
|
95
|
+
kind,
|
|
96
|
+
framework,
|
|
97
|
+
name: spec.name,
|
|
98
|
+
root: `${root}/${spec.name}/design`,
|
|
99
|
+
next,
|
|
100
|
+
};
|
|
101
|
+
if (opts.format === 'text') {
|
|
102
|
+
return textReport([
|
|
103
|
+
`created ${report.root} (framework ${framework}, kind ${kind})`,
|
|
104
|
+
`next: ${report.next.join(' | ')}`,
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
return { stdout: canonicalJson(report) + '\n', exit: EXIT.ok };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function opLiveCreate(opts, spec) {
|
|
111
|
+
return opDesignCreate(opts, { ...spec, kind: 'live' });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function opDesignStart(opts, spec) {
|
|
115
|
+
const { config } = opts;
|
|
116
|
+
const host = spec.host ?? config.review.host;
|
|
117
|
+
const port = spec.port ?? config.review.port;
|
|
118
|
+
const session = spec.session ?? 'default';
|
|
119
|
+
const environment = spec.environment ?? 'design';
|
|
120
|
+
const root = resolve(opts.cwd, spec.dir ?? config.design.root);
|
|
121
|
+
const url = `http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${port}`;
|
|
122
|
+
const plan = {
|
|
123
|
+
action: 'start',
|
|
124
|
+
kind: 'design',
|
|
125
|
+
mode: 'design',
|
|
126
|
+
root: relPath(opts.cwd, root),
|
|
127
|
+
host,
|
|
128
|
+
port,
|
|
129
|
+
session_id: session,
|
|
130
|
+
environment,
|
|
131
|
+
url,
|
|
132
|
+
};
|
|
133
|
+
return startServer(opts, {
|
|
134
|
+
mode: 'design', host, port, session, environment, url,
|
|
135
|
+
root: relPath(opts.cwd, root), target: null,
|
|
136
|
+
dryRun: spec.dryRun ?? false, foreground: spec.foreground ?? false, plan,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { opLiveStatus as opDesignStatus, opLiveStop as opDesignStop };
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI error carrying a canonical exit code (SPC section 75).
|
|
3
|
+
*
|
|
4
|
+
* @module errors
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const EXIT = {
|
|
8
|
+
ok: 0,
|
|
9
|
+
general: 1,
|
|
10
|
+
usage: 2,
|
|
11
|
+
config: 3,
|
|
12
|
+
server: 4,
|
|
13
|
+
authorization: 5,
|
|
14
|
+
notFound: 6,
|
|
15
|
+
conflict: 7,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Error with a canonical exit code; message printed as `error: …` on stderr. */
|
|
19
|
+
export class CliError extends Error {
|
|
20
|
+
/**
|
|
21
|
+
* @param {number} code canonical exit code
|
|
22
|
+
* @param {string} message diagnostic message (no locale, no paths of the host)
|
|
23
|
+
*/
|
|
24
|
+
constructor(code, message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.code = code;
|
|
27
|
+
}
|
|
28
|
+
}
|