@systemverification/styling-kit 2.0.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/src/bundle.js ADDED
@@ -0,0 +1,67 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+ import AdmZip from 'adm-zip';
5
+
6
+ /**
7
+ * Unpack a private asset bundle zip into the target directories.
8
+ *
9
+ * Bundle layout:
10
+ * manifest.json — metadata + checksums (skipped, used for validation)
11
+ * fonts.css → stylesDir/fonts.css
12
+ * fonts/*.woff2 → assetsDir/fonts/*.woff2
13
+ * logo/logo.svg → assetsDir/logo/logo.svg
14
+ * favicon.png → assetsDir/favicon.png
15
+ *
16
+ * @param {Buffer} zipBuffer
17
+ * @param {{ assetsDir: string, stylesDir: string }} destinations — absolute paths
18
+ * @returns {{ version: string }} manifest version
19
+ * @throws if any checksum fails or manifest is missing/invalid
20
+ */
21
+ export function unpackBundle(zipBuffer, { assetsDir, stylesDir }) {
22
+ const zip = new AdmZip(zipBuffer);
23
+
24
+ const manifestEntry = zip.getEntry('manifest.json');
25
+ if (!manifestEntry) {
26
+ throw new Error('Asset bundle is missing manifest.json');
27
+ }
28
+
29
+ const manifest = JSON.parse(manifestEntry.getData().toString('utf8'));
30
+ if (!manifest.version || !manifest.checksums) {
31
+ throw new Error('Asset bundle manifest.json is malformed (missing version or checksums)');
32
+ }
33
+
34
+ // Validate checksums before writing anything
35
+ for (const [entryPath, expectedHash] of Object.entries(manifest.checksums)) {
36
+ const entry = zip.getEntry(entryPath);
37
+ if (!entry) {
38
+ throw new Error(`Bundle is missing file declared in manifest: ${entryPath}`);
39
+ }
40
+ const actualHash = crypto
41
+ .createHash('sha256')
42
+ .update(entry.getData())
43
+ .digest('hex');
44
+ if (actualHash !== expectedHash) {
45
+ throw new Error(`Checksum mismatch for ${entryPath}`);
46
+ }
47
+ }
48
+
49
+ // Write files after all checksums pass
50
+ for (const entry of zip.getEntries()) {
51
+ if (entry.isDirectory || entry.entryName === 'manifest.json') continue;
52
+
53
+ const entryName = entry.entryName;
54
+ let destPath;
55
+
56
+ if (entryName === 'fonts.css') {
57
+ destPath = path.join(stylesDir, 'fonts.css');
58
+ } else {
59
+ destPath = path.join(assetsDir, entryName);
60
+ }
61
+
62
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
63
+ fs.writeFileSync(destPath, entry.getData());
64
+ }
65
+
66
+ return { version: manifest.version };
67
+ }
package/src/config.js ADDED
@@ -0,0 +1,67 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ export const CONFIG_FILE = 'sv-style.json';
5
+ export const TOOL_VERSION = '2.0.0';
6
+ export const BLOB_DEFAULTS = {
7
+ accountName: 'svstylingkit',
8
+ containerName: 'licensed-assets',
9
+ };
10
+
11
+ export function readConfig(cwd) {
12
+ const configPath = path.join(cwd, CONFIG_FILE);
13
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
14
+ }
15
+
16
+ export function writeConfig(cwd, config) {
17
+ const configPath = path.join(cwd, CONFIG_FILE);
18
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
19
+ }
20
+
21
+ /**
22
+ * Build a fresh v2 config object for a new init.
23
+ */
24
+ export function buildConfig({ stylesDir, assetsDir, docsDir, publicDir }) {
25
+ return {
26
+ svStyle: {
27
+ toolVersion: TOOL_VERSION,
28
+ assetsVersion: null,
29
+ installMode: 'degraded',
30
+ blob: { ...BLOB_DEFAULTS },
31
+ paths: {
32
+ tokens: `${stylesDir}/tokens-core.css`,
33
+ fonts: `${stylesDir}/fonts.css`,
34
+ assets: assetsDir,
35
+ docs: docsDir,
36
+ publicDir,
37
+ },
38
+ },
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Migrate a v1 config (has svStyle.version) to v2 schema.
44
+ * Returns the config unchanged if it is already v2.
45
+ */
46
+ export function migrateConfig(config) {
47
+ if (config.svStyle.toolVersion) return config;
48
+
49
+ const { paths } = config.svStyle;
50
+ const tokensDir = paths.tokens.replace(/\/[^/]+$/, '');
51
+
52
+ return {
53
+ svStyle: {
54
+ toolVersion: TOOL_VERSION,
55
+ assetsVersion: null,
56
+ installMode: 'unknown',
57
+ blob: { ...BLOB_DEFAULTS },
58
+ paths: {
59
+ tokens: `${tokensDir}/tokens-core.css`,
60
+ fonts: `${tokensDir}/fonts.css`,
61
+ assets: paths.assets,
62
+ docs: paths.docs,
63
+ publicDir: paths.publicDir || 'public',
64
+ },
65
+ },
66
+ };
67
+ }
package/src/doctor.js ADDED
@@ -0,0 +1,113 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { DefaultAzureCredential } from '@azure/identity';
4
+ import { BlobClient } from '@azure/storage-blob';
5
+ import { CONFIG_FILE, BLOB_DEFAULTS } from './config.js';
6
+ import { classifyBlobError } from './assets.js';
7
+
8
+ const BUNDLE_BLOB_PATH = 'sv-style/assets/latest/asset-bundle.zip';
9
+
10
+ async function checkBlobAccess(blobConfig) {
11
+ const { accountName, containerName } = blobConfig ?? BLOB_DEFAULTS;
12
+ const url = `https://${accountName}.blob.core.windows.net/${containerName}/${BUNDLE_BLOB_PATH}`;
13
+ try {
14
+ await new BlobClient(url, new DefaultAzureCredential()).getProperties();
15
+ return { ok: true, message: 'Azure Blob reachable and authenticated' };
16
+ } catch (err) {
17
+ const type = classifyBlobError(err);
18
+ return { ok: false, message: `Azure Blob: ${err.message} (${type})` };
19
+ }
20
+ }
21
+
22
+ export default async function doctor(_deps = {}) {
23
+ const cwd = _deps.cwd ?? process.cwd();
24
+ const checks = [];
25
+
26
+ const pass = (name, msg) => checks.push({ name, ok: true, message: msg });
27
+ const fail = (name, msg) => checks.push({ name, ok: false, message: msg });
28
+
29
+ // 1. Config file
30
+ const configPath = path.join(cwd, CONFIG_FILE);
31
+ let config = null;
32
+ if (!fs.existsSync(configPath)) {
33
+ fail('config', 'sv-style.json not found — run sv-style init');
34
+ } else {
35
+ try {
36
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
37
+ pass('config', 'sv-style.json present and valid JSON');
38
+ } catch (e) {
39
+ fail('config', `sv-style.json invalid JSON: ${e.message}`);
40
+ }
41
+ }
42
+
43
+ if (config) {
44
+ const s = config.svStyle;
45
+
46
+ // 2. Schema version
47
+ if (s?.toolVersion) {
48
+ pass('schema', `Tool version: v${s.toolVersion}`);
49
+ } else {
50
+ fail('schema', 'Old v1 config — run sv-style update to migrate');
51
+ }
52
+
53
+ // 3. Install mode
54
+ const mode = s?.installMode;
55
+ if (mode === 'full') {
56
+ pass('install-mode', `Install mode: full (assets v${s.assetsVersion})`);
57
+ } else if (mode === 'degraded') {
58
+ fail('install-mode', 'Install mode: degraded — branded assets not installed');
59
+ } else {
60
+ fail('install-mode', `Install mode: ${mode ?? 'unknown'} — run sv-style update`);
61
+ }
62
+
63
+ // 4. Public file paths
64
+ const pathChecks = [
65
+ { key: 'tokens', label: 'tokens-core.css' },
66
+ { key: 'fonts', label: 'fonts.css' },
67
+ { key: 'docs', label: 'docs directory', isDir: true },
68
+ ];
69
+
70
+ for (const { key, label, isDir } of pathChecks) {
71
+ const rel = s?.paths?.[key];
72
+ if (!rel) { fail(key, `${label}: path not configured`); continue; }
73
+ const abs = path.join(cwd, rel);
74
+ fs.existsSync(abs)
75
+ ? pass(key, `${label} → ${rel}`)
76
+ : fail(key, `${label} missing at ${rel}`);
77
+ }
78
+
79
+ // 5. Private binary assets
80
+ const assetsDir = s?.paths?.assets;
81
+ if (assetsDir) {
82
+ const present =
83
+ fs.existsSync(path.join(cwd, assetsDir, 'logo', 'logo.svg')) &&
84
+ fs.existsSync(path.join(cwd, assetsDir, 'favicon.png')) &&
85
+ fs.existsSync(path.join(cwd, assetsDir, 'fonts', 'body-font.woff2'));
86
+ present
87
+ ? pass('private-assets', `Private assets present in ${assetsDir}`)
88
+ : fail('private-assets', `Private assets missing in ${assetsDir} — run sv-style update after az login`);
89
+ } else {
90
+ fail('private-assets', 'assets path not configured');
91
+ }
92
+
93
+ // 6. Azure Blob access (live probe)
94
+ process.stdout.write(' Probing Azure Blob...\r');
95
+ const blobResult = await checkBlobAccess(s?.blob);
96
+ blobResult.ok
97
+ ? pass('blob-access', blobResult.message)
98
+ : fail('blob-access', blobResult.message);
99
+ }
100
+
101
+ // Print summary
102
+ const passed = checks.filter(c => c.ok).length;
103
+ console.log('\n── SV Style Doctor ──────────────────────────────────────');
104
+ for (const { ok, message } of checks) {
105
+ console.log(` ${ok ? '✓' : '✗'} ${message}`);
106
+ }
107
+ console.log('─────────────────────────────────────────────────────────');
108
+ console.log(` ${passed}/${checks.length} checks passed`);
109
+ if (passed < checks.length) {
110
+ console.log(` Tip: run 'sv-style update' (after 'az login' if needed)`);
111
+ }
112
+ console.log('');
113
+ }
@@ -0,0 +1,15 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ export function copyDir(src, dest) {
5
+ fs.mkdirSync(dest, { recursive: true });
6
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
7
+ const srcPath = path.join(src, entry.name);
8
+ const destPath = path.join(dest, entry.name);
9
+ if (entry.isDirectory()) {
10
+ copyDir(srcPath, destPath);
11
+ } else {
12
+ fs.copyFileSync(srcPath, destPath);
13
+ }
14
+ }
15
+ }
package/src/init.js ADDED
@@ -0,0 +1,182 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { copyDir } from './fs-utils.js';
5
+ import { computeAssetUrlPrefix, rewriteFontPaths, rewriteDocAssetPaths } from './path-rewrite.js';
6
+ import { CONFIG_FILE, buildConfig, writeConfig } from './config.js';
7
+ import { downloadBundle } from './assets.js';
8
+ import { unpackBundle } from './bundle.js';
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const KIT_ROOT = path.resolve(__dirname, '..');
12
+
13
+ const FALLBACK_FONTS_CSS = `/* SV Style System — Fallback Font Declarations (degraded mode)
14
+ Private licensed fonts are not installed. Using system fonts.
15
+ Run 'sv-style update' after authenticating with Azure to install branded fonts.
16
+ See 'sv-style doctor' for current status. */
17
+
18
+ @font-face {
19
+ font-family: "BodyFont";
20
+ src: local("Segoe UI"), local("Helvetica Neue"), local("Arial");
21
+ font-weight: normal;
22
+ font-style: normal;
23
+ font-display: swap;
24
+ }
25
+
26
+ @font-face {
27
+ font-family: "BodyFont";
28
+ src: local("Segoe UI"), local("Helvetica Neue"), local("Arial");
29
+ font-weight: normal;
30
+ font-style: italic;
31
+ font-display: swap;
32
+ }
33
+
34
+ @font-face {
35
+ font-family: "HeadingFont";
36
+ src: local("Segoe UI"), local("Helvetica Neue"), local("Arial");
37
+ font-weight: normal;
38
+ font-style: normal;
39
+ font-display: swap;
40
+ }
41
+ `;
42
+
43
+ function renderSnippet(template, { docsDir, tokensPath }) {
44
+ return template
45
+ .replace(/\{\{DOCS_DIR\}\}/g, docsDir)
46
+ .replace(/\{\{TOKENS_PATH\}\}/g, tokensPath);
47
+ }
48
+
49
+ export default async function init(flags, _deps = {}) {
50
+ const _downloadBundle = _deps.downloadBundle ?? downloadBundle;
51
+ const cwd = _deps.cwd ?? process.cwd();
52
+ const _exit = _deps.exit ?? ((code) => process.exit(code));
53
+ const requireAssets = flags.requireAssets ?? false;
54
+
55
+ if (fs.existsSync(path.join(cwd, CONFIG_FILE))) {
56
+ console.error(`sv-style.json already exists. Run 'update' to sync files.`);
57
+ return _exit(1);
58
+ }
59
+
60
+ const stylesDir = flags.stylesDir || 'src/styles/sv';
61
+ const assetsDir = flags.assetsDir || 'public/sv-assets';
62
+ const docsDir = flags.docsDir || 'docs/sv-style';
63
+ const publicDir = flags.publicDir || 'public';
64
+
65
+ const tokensPath = `${stylesDir}/tokens-core.css`;
66
+ const fontsPath = `${stylesDir}/fonts.css`;
67
+ const assetUrlPrefix = computeAssetUrlPrefix(assetsDir, publicDir);
68
+
69
+ // Create destination directories
70
+ fs.mkdirSync(path.join(cwd, stylesDir), { recursive: true });
71
+ fs.mkdirSync(path.join(cwd, assetsDir), { recursive: true });
72
+ fs.mkdirSync(path.join(cwd, docsDir), { recursive: true });
73
+
74
+ // Install public files: tokens-core.css
75
+ fs.copyFileSync(
76
+ path.join(KIT_ROOT, 'tokens', 'tokens-core.css'),
77
+ path.join(cwd, tokensPath)
78
+ );
79
+
80
+ // Install docs (references to assets are rewritten to match the installed location)
81
+ copyDir(
82
+ path.join(KIT_ROOT, 'docs'),
83
+ path.join(cwd, docsDir)
84
+ );
85
+ rewriteDocAssetPaths(path.join(cwd, docsDir), assetUrlPrefix);
86
+
87
+ // Write initial config (installMode: degraded until assets confirmed)
88
+ const config = buildConfig({ stylesDir, assetsDir, docsDir, publicDir });
89
+ writeConfig(cwd, config);
90
+
91
+ // Patch AGENTS.md
92
+ const agentsPath = path.join(cwd, 'AGENTS.md');
93
+ const existingAgents = fs.existsSync(agentsPath)
94
+ ? fs.readFileSync(agentsPath, 'utf8')
95
+ : '';
96
+
97
+ if (existingAgents.includes('SV Style System')) {
98
+ console.log('AGENTS.md already contains SV Style section — skipping.');
99
+ } else {
100
+ const agentsTemplate = fs.readFileSync(
101
+ path.join(KIT_ROOT, 'templates', 'AGENTS_SNIPPET.md'),
102
+ 'utf8'
103
+ );
104
+ const agentsSnippet = renderSnippet(agentsTemplate, { docsDir, tokensPath });
105
+ const agentsSeparator = existingAgents.endsWith('\n') ? '\n' : '\n\n';
106
+ fs.writeFileSync(agentsPath, existingAgents + agentsSeparator + agentsSnippet);
107
+ console.log(`Patched AGENTS.md`);
108
+ }
109
+
110
+ // Patch CLAUDE.md
111
+ const claudePath = path.join(cwd, 'CLAUDE.md');
112
+ const existingClaude = fs.existsSync(claudePath)
113
+ ? fs.readFileSync(claudePath, 'utf8')
114
+ : '';
115
+
116
+ if (existingClaude.includes('SV Style')) {
117
+ console.log('CLAUDE.md already contains SV Style section — skipping.');
118
+ } else {
119
+ const snippetTemplate = fs.readFileSync(
120
+ path.join(KIT_ROOT, 'templates', 'CLAUDE_SNIPPET.md'),
121
+ 'utf8'
122
+ );
123
+ const snippet = renderSnippet(snippetTemplate, { docsDir, tokensPath });
124
+ const separator = existingClaude.endsWith('\n') ? '\n' : '\n\n';
125
+ fs.writeFileSync(claudePath, existingClaude + separator + snippet);
126
+ console.log(`Patched CLAUDE.md`);
127
+ }
128
+
129
+ // Attempt private asset download from Azure Blob
130
+ console.log(`\nDownloading private assets from Azure Blob...`);
131
+ try {
132
+ const zipBuffer = await _downloadBundle(config.svStyle.blob);
133
+ const { version: assetsVersion } = unpackBundle(zipBuffer, {
134
+ assetsDir: path.join(cwd, assetsDir),
135
+ stylesDir: path.join(cwd, stylesDir),
136
+ });
137
+ rewriteFontPaths(path.join(cwd, fontsPath), assetUrlPrefix);
138
+
139
+ config.svStyle.installMode = 'full';
140
+ config.svStyle.assetsVersion = assetsVersion;
141
+ writeConfig(cwd, config);
142
+
143
+ console.log(`
144
+ SV Style System installed (full):
145
+ Tokens → ${tokensPath}
146
+ Fonts → ${fontsPath}
147
+ Assets → ${assetsDir}/
148
+ Docs → ${docsDir}/
149
+ Config → ${CONFIG_FILE}
150
+ Agents → AGENTS.md, CLAUDE.md
151
+ `);
152
+ } catch (err) {
153
+ if (requireAssets) {
154
+ console.error(`\nAsset download failed (--require-assets): ${err.message}`);
155
+ return _exit(1);
156
+ }
157
+
158
+ // Degraded mode: write system font fallback
159
+ fs.writeFileSync(path.join(cwd, fontsPath), FALLBACK_FONTS_CSS);
160
+
161
+ console.warn(`
162
+ ⚠ Asset download failed — installed in degraded mode.
163
+ Reason: ${err.message}
164
+ Missing: branded fonts, logo, favicon.
165
+
166
+ To complete installation:
167
+ 1. Authenticate: az login
168
+ 2. Retry: sv-style update
169
+
170
+ Run 'sv-style doctor' for full status.
171
+ `);
172
+
173
+ console.log(`
174
+ SV Style System installed (degraded):
175
+ Tokens → ${tokensPath}
176
+ Fonts → ${fontsPath} (system font fallback)
177
+ Docs → ${docsDir}/
178
+ Config → ${CONFIG_FILE}
179
+ Agents → AGENTS.md, CLAUDE.md
180
+ `);
181
+ }
182
+ }
package/src/login.js ADDED
@@ -0,0 +1,65 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { DefaultAzureCredential } from '@azure/identity';
4
+ import { BlobClient } from '@azure/storage-blob';
5
+ import { CONFIG_FILE, BLOB_DEFAULTS } from './config.js';
6
+ import { classifyBlobError } from './assets.js';
7
+
8
+ const BUNDLE_BLOB_PATH = 'sv-style/assets/latest/asset-bundle.zip';
9
+
10
+ /**
11
+ * Test Azure Blob connectivity using the current credential chain.
12
+ * Reads blob config from sv-style.json if present, otherwise uses defaults.
13
+ */
14
+ export default async function login(_deps = {}) {
15
+ const cwd = _deps.cwd ?? process.cwd();
16
+
17
+ let blobConfig = { ...BLOB_DEFAULTS };
18
+ const configPath = path.join(cwd, CONFIG_FILE);
19
+ if (fs.existsSync(configPath)) {
20
+ try {
21
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
22
+ if (raw.svStyle?.blob) {
23
+ blobConfig = { ...blobConfig, ...raw.svStyle.blob };
24
+ }
25
+ } catch {}
26
+ }
27
+
28
+ const { accountName, containerName } = blobConfig;
29
+ const url = `https://${accountName}.blob.core.windows.net/${containerName}/${BUNDLE_BLOB_PATH}`;
30
+
31
+ console.log(`SV Style — Azure Blob access check`);
32
+ console.log(` Account: ${accountName}.blob.core.windows.net`);
33
+ console.log(` Container: ${containerName}`);
34
+ console.log(` Bundle: ${BUNDLE_BLOB_PATH}`);
35
+
36
+ const credential = new DefaultAzureCredential();
37
+ const client = new BlobClient(url, credential);
38
+
39
+ try {
40
+ const props = await client.getProperties();
41
+ console.log(`\n✓ Access verified.`);
42
+ console.log(` Bundle size: ${props.contentLength?.toLocaleString() ?? '?'} bytes`);
43
+ return true;
44
+ } catch (err) {
45
+ const type = classifyBlobError(err);
46
+ console.error(`\n✗ Access failed (${type}): ${err.message}`);
47
+ switch (type) {
48
+ case 'not-logged-in':
49
+ console.error(` → Not logged in. Run: az login`);
50
+ break;
51
+ case 'unauthorized':
52
+ console.error(` → No access. Contact your administrator to request access.`);
53
+ break;
54
+ case 'not-found':
55
+ console.error(` → Bundle not found. Check accountName/containerName in sv-style.json.`);
56
+ break;
57
+ case 'network-error':
58
+ console.error(` → Network error. Check your internet connection.`);
59
+ break;
60
+ default:
61
+ console.error(` → Unexpected error. Try again or run 'sv-style doctor'.`);
62
+ }
63
+ return false;
64
+ }
65
+ }