@astryxdesign/cli 0.6.3-canary.d0d49a4 → 0.6.3-canary.dea6813
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 +32 -18
- package/api/build/build.doc.mjs +3 -3
- package/api/build/build.test.mjs +14 -3
- package/api/build/build.type.d.mts +35 -1
- package/api/build/build.type.mjs +24 -3
- package/api/build/help/help.d.mts +8 -5
- package/api/build/help/help.mjs +56 -6
- package/api/component/component.doc.mjs +11 -2
- package/api/component/component.mjs +29 -0
- package/api/component/component.test.mjs +38 -0
- package/api/component/component.type.d.mts +11 -1
- package/api/component/component.type.mjs +8 -1
- package/api/discover/discover.doc.mjs +1 -1
- package/api/gap-report/gap-report.doc.mjs +7 -4
- package/api/init/init.doc.mjs +4 -0
- package/api/init/init.test.mjs +41 -1
- package/api/init/remove/remove.mjs +1 -1
- package/api/init/run/run.mjs +20 -10
- package/api/integration/add-contribution.component-names.test.mjs +120 -0
- package/api/integration/add-contribution.mjs +11 -0
- package/api/integration/add-contribution.test.mjs +80 -0
- package/api/integration/integrationAdd.doc.mjs +7 -4
- package/api/integration/integrationAddComponent.doc.mjs +1 -1
- package/api/integration/validate-integration.mjs +2 -0
- package/api/integration/validate-integration.test.mjs +91 -0
- package/api/integration/validateIntegration.doc.mjs +1 -1
- package/api/json/envelope-types.test.mjs +76 -0
- package/api/json/parseResponse.doc.mjs +2 -2
- package/api/layout/expand/expand.mjs +7 -5
- package/api/layout/expand/expand.path-safety.test.mjs +53 -0
- package/api/layout/grammar/grammar.mjs +2 -1
- package/api/layout/layoutExpand.doc.mjs +1 -1
- package/api/search/search-return-type.test.mjs +54 -0
- package/api/search/search.d.mts +2 -9
- package/api/search/search.doc.mjs +5 -1
- package/api/search/search.mjs +10 -3
- package/api/search/search.test.mjs +34 -0
- package/api/swizzle/copy/copy.mjs +28 -11
- package/api/swizzle/swizzle.doc.mjs +1 -1
- package/api/template/copy/copy.mjs +17 -23
- package/api/template/copy/copy.test.mjs +17 -0
- package/api/template/template.doc.mjs +2 -1
- package/api/theme/add/add.binary.test.mjs +91 -0
- package/api/theme/add/add.mjs +20 -3
- package/api/theme/add/add.staging.test.mjs +66 -0
- package/api/theme/build/build.mjs +114 -37
- package/api/theme/build/build.public-component-vars.test.mjs +1 -1
- package/api/theme/build/build.receipt-doc.test.mjs +111 -0
- package/api/theme/build/font-warning.mjs +3 -3
- package/api/theme/build/font-warning.test.mjs +5 -2
- package/api/theme/palette/generate/generate.mjs +7 -2
- package/api/theme/palette/generate/generate.test.mjs +96 -0
- package/api/theme/palette/generate/generator.mjs +8 -1
- package/api/theme/palette/generate/generator.test.mjs +10 -0
- package/api/theme/template/template.mjs +11 -2
- package/api/theme/template/template.test.mjs +15 -0
- package/api/theme/themeBuild.doc.mjs +7 -4
- package/api/theme/themeTemplate.doc.mjs +4 -1
- package/api/upgrade/_adapter.mjs +27 -4
- package/api/upgrade/list/list.mjs +2 -1
- package/api/upgrade/list/list.test.mjs +73 -0
- package/api/upgrade/status/status.mjs +2 -2
- package/api/upgrade/upgrade.doc.mjs +2 -1
- package/api/upgrade/upgrade.type.d.mts +4 -0
- package/api/upgrade/upgrade.type.mjs +1 -0
- package/assets/codemods/term-log.mjs +32 -8
- package/assets/codemods/term-log.test.mjs +19 -1
- package/assets/codemods/transforms/v0.6.0/__tests__/next-codemods.test.mjs +47 -0
- package/assets/codemods/transforms/v0.6.0/rename-resizable-pixel-bounds.mjs +57 -10
- package/assets/templates/pages/table-tree/page.tsx +1704 -0
- package/assets/templates/pages/table-tree/template.doc.mjs +12 -0
- package/authoring/config/config.doc.mjs +1 -1
- package/authoring/config/debug-composition.test.mjs +92 -0
- package/authoring/config/type.ts +15 -3
- package/clients/cli/command-load-failure.test.mjs +83 -0
- package/clients/cli/commands/blog.mjs +23 -8
- package/clients/cli/commands/blog.test.mjs +42 -1
- package/clients/cli/commands/build-theme.ascii-output.test.mjs +161 -0
- package/clients/cli/commands/build-theme.flag-docs.test.mjs +110 -0
- package/clients/cli/commands/build-theme.mjs +5 -5
- package/clients/cli/commands/build-theme.path-safety.test.mjs +86 -1
- package/clients/cli/commands/build-theme.variants.test.mjs +76 -0
- package/clients/cli/commands/build.doc.mjs +4 -1
- package/clients/cli/commands/build.exit-codes-doc.test.mjs +50 -0
- package/clients/cli/commands/build.mjs +32 -47
- package/clients/cli/commands/build.playbook.test.mjs +75 -0
- package/clients/cli/commands/build.text-fields.test.mjs +40 -0
- package/clients/cli/commands/component/index.mjs +2 -7
- package/clients/cli/commands/component-package.test.mjs +18 -0
- package/clients/cli/commands/component-resolution.test.mjs +21 -0
- package/clients/cli/commands/component.test.mjs +19 -0
- package/clients/cli/commands/detail-levels.test.mjs +2 -2
- package/clients/cli/commands/discover.broken-integration.test.mjs +48 -0
- package/clients/cli/commands/discover.components-flag.test.mjs +97 -0
- package/clients/cli/commands/discover.doc.mjs +4 -2
- package/clients/cli/commands/discover.mjs +3 -3
- package/clients/cli/commands/discover.text-projection.test.mjs +103 -0
- package/clients/cli/commands/doctor-integration.package-json.test.mjs +53 -0
- package/clients/cli/commands/doctor.mjs +6 -16
- package/clients/cli/commands/doctor.test.mjs +42 -0
- package/clients/cli/commands/gap-report.doc.mjs +16 -5
- package/clients/cli/commands/gap-report.test.mjs +72 -0
- package/clients/cli/commands/hook/index.mjs +7 -17
- package/clients/cli/commands/hook.text-projection.test.mjs +45 -0
- package/clients/cli/commands/init.doc.mjs +19 -8
- package/clients/cli/commands/init.flag-help.test.mjs +153 -0
- package/clients/cli/commands/integration-add.controls.test.mjs +132 -0
- package/clients/cli/commands/integration-add.doc.mjs +21 -5
- package/clients/cli/commands/interactive-guard.test.mjs +101 -24
- package/clients/cli/commands/json-contract.test.mjs +33 -0
- package/clients/cli/commands/layout-check.doc.mjs +14 -3
- package/clients/cli/commands/layout-expand.doc.mjs +21 -4
- package/clients/cli/commands/layout.doc.mjs +2 -2
- package/clients/cli/commands/layout.mjs +21 -9
- package/clients/cli/commands/layout.path-help.test.mjs +33 -0
- package/clients/cli/commands/layout.stdin-cap.test.mjs +47 -0
- package/clients/cli/commands/layout.text-fields.test.mjs +39 -0
- package/clients/cli/commands/no-prompt-wording.test.mjs +94 -0
- package/clients/cli/commands/search.doc.mjs +6 -3
- package/clients/cli/commands/search.mjs +17 -7
- package/clients/cli/commands/search.test.mjs +75 -0
- package/clients/cli/commands/swizzle.doc.mjs +2 -1
- package/clients/cli/commands/swizzle.path-safety.test.mjs +42 -0
- package/clients/cli/commands/template.doc.mjs +28 -6
- package/clients/cli/commands/template.flag-help.test.mjs +117 -0
- package/clients/cli/commands/template.path-help.test.mjs +40 -0
- package/clients/cli/commands/theme-add.doc.mjs +2 -1
- package/clients/cli/commands/theme-build.doc.mjs +7 -6
- package/clients/cli/commands/theme-palette-generate.test.mjs +19 -0
- package/clients/cli/commands/theme-template.behavior.test.mjs +12 -0
- package/clients/cli/commands/theme-template.doc.mjs +1 -1
- package/clients/cli/commands/upgrade.ascii-output.test.mjs +87 -0
- package/clients/cli/commands/upgrade.doc.mjs +17 -6
- package/clients/cli/commands/upgrade.flag-help.test.mjs +188 -0
- package/clients/cli/commands/upgrade.hook-output.test.mjs +88 -0
- package/clients/cli/index.mjs +13 -30
- package/clients/cli/latest-version-env.test.mjs +50 -0
- package/clients/cli/lib/cli-error.test.mjs +7 -0
- package/clients/cli/lib/component-format.mjs +9 -9
- package/clients/cli/lib/component-format.test.mjs +1 -1
- package/clients/cli/lib/define-command.mjs +32 -6
- package/clients/cli/lib/doc-text-ascii.test.mjs +82 -0
- package/clients/cli/lib/exit-codes.test.mjs +97 -0
- package/clients/cli/lib/hook-format.mjs +5 -5
- package/clients/cli/lib/json-shim.mjs +38 -2
- package/clients/cli/lib/json-shim.test.mjs +83 -0
- package/clients/cli/lib/manifest.d.ts +2 -0
- package/clients/cli/lib/manifest.mjs +22 -0
- package/clients/cli/lib/manifest.test.mjs +17 -0
- package/foundation/agent-docs/agent-docs.d.mts +4 -0
- package/foundation/agent-docs/agent-docs.mjs +69 -3
- package/foundation/agent-docs/agent-docs.path-safety.test.mjs +266 -4
- package/foundation/config/integration-debug.test.mjs +28 -3
- package/foundation/config/project.mjs +123 -9
- package/foundation/config/project.test.mjs +126 -0
- package/foundation/discovery/template-adapter.d.mts +14 -5
- package/foundation/discovery/template-adapter.fixture-refs.test.mjs +248 -0
- package/foundation/discovery/template-adapter.integration-isolation.test.mjs +94 -0
- package/foundation/discovery/template-adapter.mjs +292 -25
- package/foundation/discovery/theming-targets.test.mjs +4 -0
- package/foundation/fs/module-loader.d.mts +1 -0
- package/foundation/fs/module-loader.mjs +50 -1
- package/foundation/fs/module-loader.stdout.test.mjs +332 -0
- package/foundation/fs/path-safety.d.mts +3 -2
- package/foundation/fs/path-safety.mjs +49 -19
- package/foundation/fs/path-safety.test.mjs +50 -0
- package/foundation/fs/publish-file-hardlink-unavailable.test.mjs +129 -94
- package/foundation/integrations/autolink.mjs +6 -1
- package/foundation/response/base.d.ts +8 -4
- package/foundation/response/error-codes.doc.mjs +1 -2
- package/foundation/response/error-codes.mjs +1 -1
- package/foundation/response/error-codes.test.mjs +83 -0
- package/foundation/response/json-contract.test.mjs +11 -0
- package/foundation/response/json.d.mts +4 -2
- package/foundation/response/json.mjs +8 -10
- package/foundation/response/response-types.doc.mjs +3 -3
- package/foundation/xle/expand.d.mts +2 -0
- package/foundation/xle/expand.mjs +3 -2
- package/foundation/xle/expand.test.mjs +54 -0
- package/package.json +9 -9
- package/clients/cli/lib/update-check.mjs +0 -83
- package/clients/cli/lib/update-check.test.mjs +0 -137
- package/clients/cli/update-hint-commands.test.mjs +0 -54
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
|
|
4
|
+
export const doc = {
|
|
5
|
+
type: 'page',
|
|
6
|
+
name: 'Tree Table',
|
|
7
|
+
displayName: 'Tree Table',
|
|
8
|
+
description:
|
|
9
|
+
'Hierarchical table where every parent row rolls up from its children — a code repository whose folders show the newest commit beneath them, beside a detail sidebar and a rendered README. Columns resize, siblings sort within their own level, arrow keys walk the rows, and search prunes the tree to matching branches. Tree, hierarchy, nested rows, drilldown, expand, collapse, resizable, expandable, file browser, repository, rolled-up parents.',
|
|
10
|
+
isReady: true,
|
|
11
|
+
category: 'Table - Tree/Hierarchical List',
|
|
12
|
+
};
|
|
@@ -48,7 +48,7 @@ export const doc = {
|
|
|
48
48
|
name: 'debug',
|
|
49
49
|
type: '(event: DebugEvent) => void',
|
|
50
50
|
description:
|
|
51
|
-
'Record every command run. The handler is synchronous; promises are not awaited and output goes to stderr. Declare `debug` directly in this file so early commands can discover it. An integration can supply one too, as a `debug` named export from its manifest
|
|
51
|
+
'Record every command run. The handler is synchronous; promises are not awaited and output goes to stderr. Declare `debug` directly in this file so early commands can discover it. An integration can supply one too, as a `debug` named export from its manifest. Every handler runs: yours first, then each integration\'s in load order (the `integrations` list, then autolinked ones). A handler that throws is skipped; the others still run and the command\'s result does not change. Set `{"astryx": {"inheritDebug": false}}` in package.json to take only your own.',
|
|
52
52
|
example:
|
|
53
53
|
"event => appendFileSync('runs.ndjson', JSON.stringify(event) + '\\n')",
|
|
54
54
|
},
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file `debug` accepts handlers from integrations, so both public entries for
|
|
5
|
+
* it — the exported `AstryxConfig` type and `astryx docs authoring config` —
|
|
6
|
+
* must state how those handlers combine with the app's own: the contribution
|
|
7
|
+
* form, the order, what a failing handler does, and the refusal control.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import {fileURLToPath} from 'node:url';
|
|
13
|
+
import ts from 'typescript';
|
|
14
|
+
import {describe, expect, it} from 'vitest';
|
|
15
|
+
import {doc} from './config.doc.mjs';
|
|
16
|
+
|
|
17
|
+
const TYPE_FILE = path.join(
|
|
18
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
19
|
+
'type.ts',
|
|
20
|
+
);
|
|
21
|
+
const source = ts.createSourceFile(
|
|
22
|
+
TYPE_FILE,
|
|
23
|
+
fs.readFileSync(TYPE_FILE, 'utf8'),
|
|
24
|
+
ts.ScriptTarget.Latest,
|
|
25
|
+
true,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
/** @param {ts.Node} node @returns {string} */
|
|
29
|
+
function jsdocText(node) {
|
|
30
|
+
return ts
|
|
31
|
+
.getJSDocCommentsAndTags(node)
|
|
32
|
+
.map(jsdoc => ts.getTextOfJSDocComment(jsdoc.comment) ?? '')
|
|
33
|
+
.join('\n');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @param {(node: ts.Node) => boolean} match @returns {ts.Node} */
|
|
37
|
+
function findTopLevel(match) {
|
|
38
|
+
const node = source.statements.find(match);
|
|
39
|
+
if (!node) throw new Error('declaration not found in config/type.ts');
|
|
40
|
+
return node;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const configInterface = /** @type {ts.InterfaceDeclaration} */ (
|
|
44
|
+
findTopLevel(
|
|
45
|
+
node => ts.isInterfaceDeclaration(node) && node.name.text === 'AstryxConfig',
|
|
46
|
+
)
|
|
47
|
+
);
|
|
48
|
+
const debugMember = configInterface.members.find(
|
|
49
|
+
member =>
|
|
50
|
+
ts.isPropertySignature(member) && member.name.getText(source) === 'debug',
|
|
51
|
+
);
|
|
52
|
+
if (!debugMember) throw new Error('AstryxConfig has no debug member');
|
|
53
|
+
|
|
54
|
+
const debugConfigAlias = findTopLevel(
|
|
55
|
+
node => ts.isTypeAliasDeclaration(node) && node.name.text === 'DebugConfig',
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const schemaField = doc.fields.find(field => field.name === 'debug');
|
|
59
|
+
|
|
60
|
+
const ENTRIES = {
|
|
61
|
+
'the AstryxConfig type': jsdocText(debugMember),
|
|
62
|
+
'astryx docs authoring config': schemaField?.description ?? '',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
describe.each(Object.entries(ENTRIES))('debug entry in %s', (_, text) => {
|
|
66
|
+
it('says integrations contribute, and in what form', () => {
|
|
67
|
+
expect(text).toMatch(/integration/i);
|
|
68
|
+
expect(text).toMatch(/`debug` named export/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('says every handler runs, the app first, then integrations in load order', () => {
|
|
72
|
+
expect(text).toMatch(/\bfirst\b/i);
|
|
73
|
+
expect(text).toMatch(/load order/i);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('says a handler that throws is skipped without affecting the others', () => {
|
|
77
|
+
expect(text).toMatch(/throws is skipped/i);
|
|
78
|
+
expect(text).toMatch(/others still run/i);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('names the one control that refuses inherited handlers', () => {
|
|
82
|
+
expect(text).toContain('{"astryx": {"inheritDebug": false}}');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('DebugConfig', () => {
|
|
87
|
+
it('does not claim that leaving debug out records nothing', () => {
|
|
88
|
+
const text = jsdocText(debugConfigAlias);
|
|
89
|
+
expect(text).not.toMatch(/nothing is recorded/i);
|
|
90
|
+
expect(text).toMatch(/integration/i);
|
|
91
|
+
});
|
|
92
|
+
});
|
package/authoring/config/type.ts
CHANGED
|
@@ -56,8 +56,9 @@ export interface XleComponent {
|
|
|
56
56
|
/**
|
|
57
57
|
* Record every astryx command run in this project.
|
|
58
58
|
*
|
|
59
|
-
* A function that receives each run.
|
|
60
|
-
*
|
|
59
|
+
* A function that receives each run. Leaving it out does not stop a handler an
|
|
60
|
+
* integration contributes; see {@link AstryxConfig.debug} for how the two
|
|
61
|
+
* combine and how to refuse inherited handlers.
|
|
61
62
|
*
|
|
62
63
|
* ```
|
|
63
64
|
* export default {
|
|
@@ -80,7 +81,18 @@ export interface AstryxConfig {
|
|
|
80
81
|
hooks?: {
|
|
81
82
|
postCodemod?: PostCodemodHook[];
|
|
82
83
|
};
|
|
83
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Record every astryx command run in this project. See {@link DebugConfig}.
|
|
86
|
+
*
|
|
87
|
+
* An integration can contribute a handler too, as a `debug` named export
|
|
88
|
+
* from its `astryx.integration.*` module. Every handler receives the run:
|
|
89
|
+
* this one first, then each integration's in load order (the ones
|
|
90
|
+
* `integrations` lists, in that order, then autolinked ones). A handler that
|
|
91
|
+
* throws is skipped; the others still run and the command's result does not
|
|
92
|
+
* change. To refuse inherited handlers, set
|
|
93
|
+
* `{"astryx": {"inheritDebug": false}}` in the project's package.json; this
|
|
94
|
+
* handler still runs.
|
|
95
|
+
*/
|
|
84
96
|
debug?: DebugConfig;
|
|
85
97
|
/** Route gap reports through a project-owned handler. See {@link GapReportHandler}. */
|
|
86
98
|
gapReport?: GapReportHandler;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file A command whose module fails to import still answers: with --json, one
|
|
5
|
+
* error envelope on stdout; without it, the error on stderr. Exit 1 either way.
|
|
6
|
+
*
|
|
7
|
+
* The failure is injected with a module-load hook in a real subprocess, so the
|
|
8
|
+
* lazy command registry in index.mjs sees what a broken install would give it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {describe, it, expect, beforeAll, afterAll} from 'vitest';
|
|
12
|
+
import {spawnSync} from 'node:child_process';
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as os from 'node:os';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
import {fileURLToPath, pathToFileURL} from 'node:url';
|
|
17
|
+
|
|
18
|
+
const BIN = fileURLToPath(new URL('./bin/astryx.mjs', import.meta.url));
|
|
19
|
+
|
|
20
|
+
/** @type {string} */
|
|
21
|
+
let dir;
|
|
22
|
+
/** @type {string} */
|
|
23
|
+
let register;
|
|
24
|
+
|
|
25
|
+
beforeAll(() => {
|
|
26
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-load-failure-'));
|
|
27
|
+
const hooks = path.join(dir, 'hooks.mjs');
|
|
28
|
+
fs.writeFileSync(
|
|
29
|
+
hooks,
|
|
30
|
+
[
|
|
31
|
+
'export async function load(url, context, nextLoad) {',
|
|
32
|
+
" if (url.endsWith('/clients/cli/commands/blog.mjs')) {",
|
|
33
|
+
" throw new Error('simulated load failure');",
|
|
34
|
+
' }',
|
|
35
|
+
' return nextLoad(url, context);',
|
|
36
|
+
'}',
|
|
37
|
+
'',
|
|
38
|
+
].join('\n'),
|
|
39
|
+
);
|
|
40
|
+
register = path.join(dir, 'register.mjs');
|
|
41
|
+
fs.writeFileSync(
|
|
42
|
+
register,
|
|
43
|
+
`import {register} from 'node:module';\nregister(${JSON.stringify(pathToFileURL(hooks).href)});\n`,
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterAll(() => {
|
|
48
|
+
fs.rmSync(dir, {recursive: true, force: true});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/** @param {string[]} args */
|
|
52
|
+
function run(args) {
|
|
53
|
+
const res = spawnSync(
|
|
54
|
+
process.execPath,
|
|
55
|
+
['--import', pathToFileURL(register).href, BIN, ...args],
|
|
56
|
+
{cwd: dir, encoding: 'utf-8', timeout: 20_000},
|
|
57
|
+
);
|
|
58
|
+
return {status: res.status, stdout: res.stdout, stderr: res.stderr};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe('a command that fails to load', () => {
|
|
62
|
+
it('answers --json with one error envelope, exit 1', () => {
|
|
63
|
+
const {status, stdout, stderr} = run(['blog', '--json']);
|
|
64
|
+
expect(status).toBe(1);
|
|
65
|
+
expect(stderr).toBe('');
|
|
66
|
+
const envelope = JSON.parse(stdout);
|
|
67
|
+
expect(envelope).toMatchObject({apiVersion: 1, code: 'ERR_UNKNOWN'});
|
|
68
|
+
expect(envelope.error).toMatch(/blog.*simulated load failure/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('reports the failure on stderr without --json, exit 1', () => {
|
|
72
|
+
const {status, stdout, stderr} = run(['blog']);
|
|
73
|
+
expect(status).toBe(1);
|
|
74
|
+
expect(stdout).toBe('');
|
|
75
|
+
expect(stderr).toMatch(/blog.*failed to load.*simulated load failure/s);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('leaves the other commands working', () => {
|
|
79
|
+
const {status, stdout} = run(['docs', '--json']);
|
|
80
|
+
expect(status).toBe(0);
|
|
81
|
+
expect(JSON.parse(stdout).type).toMatch(/^docs\./);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -24,6 +24,18 @@ import {resultSet} from '../../../foundation/debug/index.mjs';
|
|
|
24
24
|
import {doc as blogCommand} from './blog.doc.mjs';
|
|
25
25
|
import {doc as blogFn} from '../../../api/blog/blog.doc.mjs';
|
|
26
26
|
|
|
27
|
+
/** Every field of a post in the JSON, in order; record() skips empty ones. */
|
|
28
|
+
const POST_FIELDS = [
|
|
29
|
+
'slug',
|
|
30
|
+
'title',
|
|
31
|
+
'description',
|
|
32
|
+
'date',
|
|
33
|
+
'type',
|
|
34
|
+
'authors',
|
|
35
|
+
'link',
|
|
36
|
+
'textUrl',
|
|
37
|
+
];
|
|
38
|
+
|
|
27
39
|
/**
|
|
28
40
|
* @param {import('commander').Command} program
|
|
29
41
|
*/
|
|
@@ -62,22 +74,25 @@ export function registerBlog(program) {
|
|
|
62
74
|
const {feedUrl, posts} = result.data;
|
|
63
75
|
if (posts.length === 0) {
|
|
64
76
|
emit(
|
|
65
|
-
section('Astryx blog'
|
|
77
|
+
section('Astryx blog'),
|
|
78
|
+
record({feedUrl}),
|
|
66
79
|
text('No posts found in the feed.'),
|
|
67
80
|
);
|
|
68
81
|
return answered;
|
|
69
82
|
}
|
|
70
|
-
// One record per post — fields mirror the JSON post shape; empty
|
|
71
|
-
// fields (type/textUrl) are skipped by record().
|
|
72
83
|
emit(
|
|
73
|
-
section('Astryx blog'
|
|
74
|
-
|
|
84
|
+
section('Astryx blog'),
|
|
85
|
+
record({feedUrl}),
|
|
86
|
+
records(posts, {fields: POST_FIELDS}),
|
|
75
87
|
text(`Read one: ${run} astryx blog <slug>`),
|
|
76
88
|
);
|
|
77
89
|
} else {
|
|
78
|
-
// blog.detail — the feed URL, then the
|
|
79
|
-
// (code() so article typography/spacing isn't ASCII-normalized).
|
|
80
|
-
emit(
|
|
90
|
+
// blog.detail — the post's fields and the feed URL, then the body
|
|
91
|
+
// verbatim (code() so article typography/spacing isn't ASCII-normalized).
|
|
92
|
+
emit(
|
|
93
|
+
record(result.data, {fields: [...POST_FIELDS, 'feedUrl']}),
|
|
94
|
+
code(result.data.text),
|
|
95
|
+
);
|
|
81
96
|
}
|
|
82
97
|
return answered;
|
|
83
98
|
},
|
|
@@ -19,7 +19,10 @@ const FEED = `<?xml version="1.0" encoding="UTF-8"?>
|
|
|
19
19
|
<item>
|
|
20
20
|
<title>How Astryx works</title>
|
|
21
21
|
<link>${SITE_URL}/blog/how-astryx-works</link>
|
|
22
|
+
<description>Under the hood & more</description>
|
|
22
23
|
<category>engineering</category>
|
|
24
|
+
<author>Ada</author>
|
|
25
|
+
<author>Grace</author>
|
|
23
26
|
<pubDate>Mon, 29 Jun 2026 00:00:00 GMT</pubDate>
|
|
24
27
|
<atom:link rel="alternate" type="text/plain" href="${SITE_URL}/blog/how-astryx-works.txt" />
|
|
25
28
|
</item>
|
|
@@ -39,6 +42,23 @@ function stubFetch(routes) {
|
|
|
39
42
|
});
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Every non-empty field prints as a `key: value` line under its JSON key.
|
|
47
|
+
* @param {string} stdout
|
|
48
|
+
* @param {Record<string, unknown>} fields
|
|
49
|
+
*/
|
|
50
|
+
function expectFields(stdout, fields) {
|
|
51
|
+
const lines = stdout.split('\n');
|
|
52
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
53
|
+
if (value == null || value === '' || (Array.isArray(value) && !value.length)) continue;
|
|
54
|
+
const shown = Array.isArray(value) ? value.join(', ') : String(value);
|
|
55
|
+
expect(
|
|
56
|
+
lines.some(line => line.startsWith(`${key}:`) && line.trimEnd().endsWith(shown)),
|
|
57
|
+
`${key}: ${shown}`,
|
|
58
|
+
).toBe(true);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
beforeEach(() => {
|
|
43
63
|
vi.stubGlobal(
|
|
44
64
|
'fetch',
|
|
@@ -83,10 +103,31 @@ describe('blog CLI — json-enabled', () => {
|
|
|
83
103
|
const {status, stdout} = await runCli(['blog']);
|
|
84
104
|
expect(status).toBe(0);
|
|
85
105
|
expect(stdout).toMatch(/Astryx blog/);
|
|
86
|
-
expect(stdout).toMatch(
|
|
106
|
+
expect(stdout).toMatch(/^feedUrl:/m);
|
|
87
107
|
expect(stdout).toMatch(/how-astryx-works/);
|
|
88
108
|
});
|
|
89
109
|
|
|
110
|
+
it('the text list projects every JSON field under its JSON key', async () => {
|
|
111
|
+
const env = JSON.parse((await runCli(['blog', '--json'])).stdout);
|
|
112
|
+
const {status, stdout} = await runCli(['blog']);
|
|
113
|
+
expect(status).toBe(0);
|
|
114
|
+
expectFields(stdout, {feedUrl: env.data.feedUrl});
|
|
115
|
+
for (const post of env.data.posts) expectFields(stdout, post);
|
|
116
|
+
expect(stdout).not.toMatch(/^feed:/m);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('the text detail projects every JSON field, then the post body', async () => {
|
|
120
|
+
const env = JSON.parse(
|
|
121
|
+
(await runCli(['blog', 'how-astryx-works', '--json'])).stdout,
|
|
122
|
+
);
|
|
123
|
+
const {status, stdout} = await runCli(['blog', 'how-astryx-works']);
|
|
124
|
+
expect(status).toBe(0);
|
|
125
|
+
const {text, ...fields} = env.data;
|
|
126
|
+
expectFields(stdout, fields);
|
|
127
|
+
expect(stdout).toContain(text);
|
|
128
|
+
expect(stdout).not.toMatch(/^feed:/m);
|
|
129
|
+
});
|
|
130
|
+
|
|
90
131
|
it('surfaces the API error code and exits non-zero for an unknown slug (--json)', async () => {
|
|
91
132
|
const {status, stdout} = await runCli(['blog', 'no-such-post', '--json']);
|
|
92
133
|
expect(status).not.toBe(0);
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Human output of the `theme` command group stays plain ASCII.
|
|
5
|
+
*
|
|
6
|
+
* Covers every theme-build line family that carried a glyph or typographic
|
|
7
|
+
* punctuation: success, warning, error, notice, and font-help lines of a
|
|
8
|
+
* standalone build; `--check` in both states; a batch build and check; a
|
|
9
|
+
* family build and check; the quoted-glob suggestion; and a palette preview.
|
|
10
|
+
* Each case also asserts its ASCII marker, so a line that stops printing
|
|
11
|
+
* cannot pass the ASCII check vacuously.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {describe, it, expect, beforeAll, beforeEach, afterEach} from 'vitest';
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import {ensureCoreBuilt} from './ensure-core-built.mjs';
|
|
18
|
+
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
19
|
+
|
|
20
|
+
// Under the CLI package so fixtures resolve `@astryxdesign/core/theme`.
|
|
21
|
+
const CLI_ROOT = path.resolve(import.meta.dirname, '../../..');
|
|
22
|
+
|
|
23
|
+
const OCEAN = `import {defineTheme} from '@astryxdesign/core/theme';
|
|
24
|
+
export const oceanTheme = defineTheme({
|
|
25
|
+
name: 'ocean',
|
|
26
|
+
typography: {body: {family: 'Inter'}},
|
|
27
|
+
tokens: {'--color-background-body': '#ffffff'},
|
|
28
|
+
components: {
|
|
29
|
+
progressbar: {base: {color: 'red'}},
|
|
30
|
+
button: {base: {'--_button-radius': '2px'}},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
`;
|
|
34
|
+
const CALM = `import {defineTheme} from '@astryxdesign/core/theme';
|
|
35
|
+
import {oceanTheme} from './ocean.mjs';
|
|
36
|
+
export const oceanCalmTheme = defineTheme({
|
|
37
|
+
name: 'ocean-calm',
|
|
38
|
+
extends: oceanTheme,
|
|
39
|
+
tokens: {'--color-background-body': '#eeeeee'},
|
|
40
|
+
});
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
/** @type {string} */
|
|
44
|
+
let dir;
|
|
45
|
+
|
|
46
|
+
beforeAll(() => {
|
|
47
|
+
ensureCoreBuilt();
|
|
48
|
+
}, 200_000);
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
dir = fs.mkdtempSync(path.join(CLI_ROOT, '.tmp-theme-ascii-'));
|
|
52
|
+
fs.writeFileSync(path.join(dir, 'package.json'), '{"type":"module"}\n');
|
|
53
|
+
fs.writeFileSync(path.join(dir, 'ocean.mjs'), OCEAN);
|
|
54
|
+
fs.writeFileSync(path.join(dir, 'ocean-calm.mjs'), CALM);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
fs.rmSync(dir, {recursive: true, force: true});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/** @param {{stdout: string, stderr: string}} result */
|
|
62
|
+
function nonAscii(result) {
|
|
63
|
+
return [
|
|
64
|
+
...new Set(`${result.stdout}${result.stderr}`.match(/[\u0080-\uffff]/g)),
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @param {string[]} args */
|
|
69
|
+
function run(args) {
|
|
70
|
+
return runCli(['theme', ...args], dir);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe('theme command human output is plain ASCII', () => {
|
|
74
|
+
it('a standalone build: ok, warning, error, notice, and font help lines', async () => {
|
|
75
|
+
const result = await run(['build', 'ocean.mjs']);
|
|
76
|
+
|
|
77
|
+
expect(result.code).toBe(0);
|
|
78
|
+
expect(result.stdout).toContain('[ok] ocean.css');
|
|
79
|
+
expect(result.stderr).toContain('[warn] Deprecated component target');
|
|
80
|
+
expect(result.stderr).toContain('[error] Component "button"');
|
|
81
|
+
expect(result.stdout).toContain('note: Font "Inter"');
|
|
82
|
+
expect(result.stdout).toContain('[note] Theme "ocean" names fonts');
|
|
83
|
+
expect(nonAscii(result)).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('--check, up to date and stale', async () => {
|
|
87
|
+
await run(['build', 'ocean.mjs']);
|
|
88
|
+
const upToDate = await run(['build', 'ocean.mjs', '--check']);
|
|
89
|
+
expect(upToDate.code).toBe(0);
|
|
90
|
+
expect(upToDate.stdout).toContain('[ok] Theme outputs are up to date');
|
|
91
|
+
expect(nonAscii(upToDate)).toEqual([]);
|
|
92
|
+
|
|
93
|
+
fs.appendFileSync(path.join(dir, 'ocean.css'), '/* drift */\n');
|
|
94
|
+
const stale = await run(['build', 'ocean.mjs', '--check']);
|
|
95
|
+
expect(stale.code).toBe(1);
|
|
96
|
+
expect(stale.stderr).toContain('[fail] 1 theme output(s) are out of date');
|
|
97
|
+
expect(nonAscii(stale)).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('a batch build and check', async () => {
|
|
101
|
+
const built = await run(['build', 'ocean.mjs', 'ocean-calm.mjs']);
|
|
102
|
+
expect(built.code).toBe(0);
|
|
103
|
+
expect(built.stdout).toContain('[ok] Built 2 themes.');
|
|
104
|
+
expect(nonAscii(built)).toEqual([]);
|
|
105
|
+
|
|
106
|
+
const checked = await run([
|
|
107
|
+
'build',
|
|
108
|
+
'ocean.mjs',
|
|
109
|
+
'ocean-calm.mjs',
|
|
110
|
+
'--check',
|
|
111
|
+
]);
|
|
112
|
+
expect(checked.code).toBe(0);
|
|
113
|
+
expect(checked.stdout).toContain('[ok] Checked 2 themes.');
|
|
114
|
+
expect(nonAscii(checked)).toEqual([]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('a family build and a stale family check', async () => {
|
|
118
|
+
const familyArgs = [
|
|
119
|
+
'build',
|
|
120
|
+
'--family',
|
|
121
|
+
'ocean.mjs',
|
|
122
|
+
'ocean-calm.mjs',
|
|
123
|
+
'--family-key',
|
|
124
|
+
'ocean-family',
|
|
125
|
+
];
|
|
126
|
+
const built = await run(familyArgs);
|
|
127
|
+
expect(built.code).toBe(0);
|
|
128
|
+
expect(built.stdout).toContain('[ok] ocean-family.css');
|
|
129
|
+
expect(nonAscii(built)).toEqual([]);
|
|
130
|
+
|
|
131
|
+
fs.rmSync(path.join(dir, 'ocean-family.js'));
|
|
132
|
+
const stale = await run([...familyArgs, '--check']);
|
|
133
|
+
expect(stale.code).toBe(1);
|
|
134
|
+
expect(stale.stderr).toContain('[fail] 1 theme family output(s)');
|
|
135
|
+
expect(nonAscii(stale)).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('the suggestion for a quoted glob', async () => {
|
|
139
|
+
const result = await run(['build', 'themes/*.mjs']);
|
|
140
|
+
|
|
141
|
+
expect(result.code).toBe(1);
|
|
142
|
+
expect(result.stderr).toContain('globs are expanded by your shell');
|
|
143
|
+
expect(nonAscii(result)).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('a palette candidate preview', async () => {
|
|
147
|
+
fs.writeFileSync(
|
|
148
|
+
path.join(dir, 'palette.config.json'),
|
|
149
|
+
JSON.stringify({
|
|
150
|
+
modeStrategy: 'dark-only',
|
|
151
|
+
stops: [10, 50, 90],
|
|
152
|
+
families: [{id: 'blue', seed: '#0074e2'}],
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
const result = await run(['palette', 'generate', 'palette.config.json']);
|
|
156
|
+
|
|
157
|
+
expect(result.code).toBe(0);
|
|
158
|
+
expect(result.stdout).toContain('1 families, 3 stops, dark');
|
|
159
|
+
expect(nonAscii(result)).toEqual([]);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Every `theme build` flag interaction is discoverable.
|
|
5
|
+
*
|
|
6
|
+
* Each combination the command refuses is refused before any work, and the
|
|
7
|
+
* refusal is named in the machine-readable manifest's description of at least
|
|
8
|
+
* one of the flags involved. Flags that fill in a value when omitted state
|
|
9
|
+
* their default there too.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {describe, it, expect, beforeAll, afterAll} from 'vitest';
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as os from 'node:os';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
17
|
+
import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
|
|
18
|
+
|
|
19
|
+
/** @type {string} */
|
|
20
|
+
let dir;
|
|
21
|
+
/** @type {Map<string, string>} flag token -> manifest description */
|
|
22
|
+
let descriptions;
|
|
23
|
+
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-theme-flag-docs-'));
|
|
26
|
+
// Refusals happen before a theme loads; the files only need to exist.
|
|
27
|
+
for (const name of ['a.mjs', 'b.mjs']) {
|
|
28
|
+
fs.writeFileSync(path.join(dir, name), 'export default {};\n');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const manifest = JSON.parse(
|
|
32
|
+
(await runCli(['--json', 'manifest'], dir)).stdout,
|
|
33
|
+
).data;
|
|
34
|
+
const theme = manifest.commands.find(command => command.name === 'theme');
|
|
35
|
+
const build = theme.subcommands.find(
|
|
36
|
+
command => command.name === 'theme build',
|
|
37
|
+
);
|
|
38
|
+
descriptions = new Map(
|
|
39
|
+
build.options.map(option => [
|
|
40
|
+
/** @type {string} */ (option.flag.match(/--[a-z-]+/)?.[0]),
|
|
41
|
+
option.description,
|
|
42
|
+
]),
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterAll(() => {
|
|
47
|
+
fs.rmSync(dir, {recursive: true, force: true});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** @param {string} flag */
|
|
51
|
+
function description(flag) {
|
|
52
|
+
const text = descriptions.get(flag);
|
|
53
|
+
expect(text, `manifest has no ${flag} option`).toBeDefined();
|
|
54
|
+
return /** @type {string} */ (text);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether `a`'s description names `b`, or `b`'s names `a`. `b` may also be a
|
|
59
|
+
* pattern for a condition that is not a flag.
|
|
60
|
+
* @param {string} a
|
|
61
|
+
* @param {string | RegExp} b
|
|
62
|
+
*/
|
|
63
|
+
function documented(a, b) {
|
|
64
|
+
const partner = typeof b === 'string' ? new RegExp(`${b}\\b`) : b;
|
|
65
|
+
if (partner.test(description(a))) return true;
|
|
66
|
+
return (
|
|
67
|
+
typeof b === 'string' &&
|
|
68
|
+
descriptions.has(b) &&
|
|
69
|
+
new RegExp(`${a}\\b`).test(description(b))
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const REFUSED = [
|
|
74
|
+
{
|
|
75
|
+
pair: ['--family', '--out'],
|
|
76
|
+
args: ['--family', 'a.mjs', 'b.mjs', '--family-key', 'k', '--out', 'x.css'],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
pair: ['--family', '--watch'],
|
|
80
|
+
args: ['--family', 'a.mjs', 'b.mjs', '--family-key', 'k', '--watch'],
|
|
81
|
+
},
|
|
82
|
+
{pair: ['--family', '--family-key'], args: ['--family', 'a.mjs', 'b.mjs']},
|
|
83
|
+
{pair: ['--family-key', '--family'], args: ['a.mjs', '--family-key', 'k']},
|
|
84
|
+
{pair: ['--check', '--watch'], args: ['a.mjs', '--check', '--watch']},
|
|
85
|
+
{
|
|
86
|
+
pair: ['--out', /several|more than one|single theme|one theme/i],
|
|
87
|
+
args: ['a.mjs', 'b.mjs', '--out', 'x.css'],
|
|
88
|
+
},
|
|
89
|
+
{pair: ['--watch', '--json'], args: ['a.mjs', '--watch']},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
describe('theme build flag interactions are documented', () => {
|
|
93
|
+
for (const {pair, args} of REFUSED) {
|
|
94
|
+
const [a, b] = pair;
|
|
95
|
+
it(`${a} with ${String(b)} is refused and documented`, async () => {
|
|
96
|
+
const result = await runCli(['--json', 'theme', 'build', ...args], dir);
|
|
97
|
+
|
|
98
|
+
expect(result.code).toBe(1);
|
|
99
|
+
expect(JSON.parse(result.stdout).code).toBe(
|
|
100
|
+
ERROR_CODES.ERR_THEME_INVALID,
|
|
101
|
+
);
|
|
102
|
+
expect(documented(a, b)).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
it('flags that fill in an omitted value state their default', () => {
|
|
107
|
+
expect(description('--out')).toMatch(/default/i);
|
|
108
|
+
expect(description('--icons-specifier')).toMatch(/default/i);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -369,7 +369,7 @@ export function registerTheme(program) {
|
|
|
369
369
|
emit(
|
|
370
370
|
section(
|
|
371
371
|
'Palette candidate',
|
|
372
|
-
`${result.data.familyCount} families
|
|
372
|
+
`${result.data.familyCount} families, ${result.data.stopCount} stops, ${result.data.modes.join(', ')}`,
|
|
373
373
|
),
|
|
374
374
|
code(serializePaletteCandidate(result.data.candidate).trimEnd()),
|
|
375
375
|
);
|
|
@@ -426,7 +426,7 @@ export function registerTheme(program) {
|
|
|
426
426
|
{
|
|
427
427
|
name: `astryx theme build ${entry.file.replace(/['"]/g, '')}`,
|
|
428
428
|
reason:
|
|
429
|
-
'globs are expanded by your shell
|
|
429
|
+
'globs are expanded by your shell; pass the pattern unquoted, or list the files',
|
|
430
430
|
},
|
|
431
431
|
]
|
|
432
432
|
: undefined,
|
|
@@ -491,7 +491,7 @@ export function registerTheme(program) {
|
|
|
491
491
|
// Non-watch: delegate to the API compiler, once per theme, in argument
|
|
492
492
|
// order. Enable human output unless in --json mode (log → stdout via
|
|
493
493
|
// humanLog, warn/error → stderr). The "Building theme from" line, the
|
|
494
|
-
//
|
|
494
|
+
// [ok]/[warn] lines, and the install instructions are all emitted from
|
|
495
495
|
// inside themeBuild via the shared logger.
|
|
496
496
|
logger.setSilent(json);
|
|
497
497
|
|
|
@@ -574,8 +574,8 @@ export function registerTheme(program) {
|
|
|
574
574
|
emit(
|
|
575
575
|
text(
|
|
576
576
|
options.check
|
|
577
|
-
? `\n${stale ? '
|
|
578
|
-
: `\n
|
|
577
|
+
? `\n${stale ? '[fail]' : '[ok]'} Checked ${entries.length} themes.`
|
|
578
|
+
: `\n[ok] Built ${entries.length} themes.`,
|
|
579
579
|
),
|
|
580
580
|
);
|
|
581
581
|
}
|