create-portfoliable 1.0.3

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.
Files changed (62) hide show
  1. package/bin/create-portfoliable.mjs +425 -0
  2. package/cli/portfoliable.mjs +836 -0
  3. package/index.html +18 -0
  4. package/package.json +102 -0
  5. package/public/favicon.ico +0 -0
  6. package/public/favicon.png +0 -0
  7. package/scripts/check-updates.mjs +31 -0
  8. package/scripts/ensure-valence-index-css.mjs +245 -0
  9. package/scripts/generate-password-hash.mjs +25 -0
  10. package/scripts/release-orchestrator.mjs +363 -0
  11. package/scripts/scaffold-case.mjs +460 -0
  12. package/scripts/scaffold-consumer.mjs +87 -0
  13. package/scripts/smoke-homeview.mjs +133 -0
  14. package/scripts/smoke-initializer.mjs +129 -0
  15. package/scripts/smoke-packed.mjs +184 -0
  16. package/scripts/switch-valence-source.mjs +162 -0
  17. package/scripts/sync-template-shared.mjs +136 -0
  18. package/scripts/terminal-ui.mjs +274 -0
  19. package/scripts/verify-integration.mjs +72 -0
  20. package/src/App.js +10015 -0
  21. package/src/header-visibility.contract.js +31 -0
  22. package/src/header-visibility.contract.test.js +41 -0
  23. package/src/main.js +12 -0
  24. package/src/parser/markdown.js +855 -0
  25. package/src/style.css +88 -0
  26. package/templates/README.md +242 -0
  27. package/templates/configs/i18n/i18n.config.js +230 -0
  28. package/templates/configs/i18n/i18n.labels.js +188 -0
  29. package/templates/configs/portfoliable.a11y.config.js +262 -0
  30. package/templates/configs/portfoliable.design.config.js +651 -0
  31. package/templates/gitignore +9 -0
  32. package/templates/index.html +20 -0
  33. package/templates/public/api/.htaccess +4 -0
  34. package/templates/public/api/README.md +27 -0
  35. package/templates/public/api/password.config.example.json +16 -0
  36. package/templates/public/api/password.config.json +16 -0
  37. package/templates/public/api/unlock-case.php +56 -0
  38. package/templates/public/favicon.ico +0 -0
  39. package/templates/public/favicon.png +0 -0
  40. package/templates/public/portfoliable-logo.svg +3 -0
  41. package/templates/scripts/add-language.mjs +253 -0
  42. package/templates/scripts/convert-video-to-mp4.mjs +207 -0
  43. package/templates/scripts/convert-wav-to-mp3.mjs +184 -0
  44. package/templates/scripts/delete-case.mjs +320 -0
  45. package/templates/scripts/delete-language.mjs +195 -0
  46. package/templates/scripts/generate-password-hash.mjs +98 -0
  47. package/templates/scripts/i18n-config-utils.mjs +123 -0
  48. package/templates/scripts/scaffold-case.mjs +444 -0
  49. package/templates/scripts/sync-locales.mjs +1048 -0
  50. package/templates/scripts/validate-content.mjs +257 -0
  51. package/templates/scripts/validate-protection.mjs +187 -0
  52. package/templates/src/cases/index.js +188 -0
  53. package/templates/src/content/about/ABOUTME.md +202 -0
  54. package/templates/src/content/cases/template-case/case.md +479 -0
  55. package/templates/src/content/cases/template-case/cover.png +0 -0
  56. package/templates/src/content/cases/template-case/he-cover.png +0 -0
  57. package/templates/src/content/cases/template-case/template-audio.mp3 +0 -0
  58. package/templates/src/content/cases/template-case/template-video.mp4 +0 -0
  59. package/templates/src/content/thumbnail-options.generated.json +1044 -0
  60. package/templates/src/main.js +23 -0
  61. package/templates/src/parser/markdown.js +805 -0
  62. package/vite.config.js +111 -0
@@ -0,0 +1,129 @@
1
+ // File: create-portfoliable/scripts/smoke-initializer.mjs
2
+ // Purpose: Smoke-test local initializer output by generating a project and validating built artifacts.
3
+ // Author: Lio Schimanko
4
+
5
+ // MARK: IMPORTS
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { spawnSync } from 'node:child_process';
10
+
11
+ // MARK: PATH CONSTANTS
12
+ // Resolves package root used to execute initializer and downstream build commands.
13
+ const projectRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
14
+ // Defines isolated temporary workspace root used by the initializer smoke test.
15
+ const tempRoot = path.join(os.tmpdir(), 'portfoliable-initializer-smoke');
16
+ // Defines output directory where the generated sample app is created.
17
+ const generatedAppDir = path.join(tempRoot, 'my-portfolio');
18
+
19
+ // MARK: ERROR HANDLING
20
+ // Exits immediately with a prefixed smoke-test failure message.
21
+ function fail(message) {
22
+ console.error(`[smoke:init] ${message}`);
23
+ process.exit(1);
24
+ }
25
+
26
+ // MARK: COMMAND EXECUTION
27
+ // Runs an external command and stops the smoke test if the command exits non-zero.
28
+ function runOrFail(command, args, cwd, env = undefined) {
29
+ // Executes child commands with inherited IO for transparent diagnostics.
30
+ const result = spawnSync(command, args, {
31
+ cwd,
32
+ stdio: 'inherit',
33
+ shell: process.platform === 'win32',
34
+ env: env ? { ...process.env, ...env } : process.env
35
+ });
36
+
37
+ if (result.status !== 0) {
38
+ fail(`Command failed: ${command} ${args.join(' ')}`);
39
+ }
40
+ }
41
+
42
+ // MARK: ASSERTION HELPER
43
+ // Verifies bundle output contains expected markers proving starter template content is present.
44
+ function ensureContains(text, needle, description) {
45
+ if (!text.includes(needle)) {
46
+ fail(`Missing expected marker (${description}): ${needle}`);
47
+ }
48
+ }
49
+
50
+ // Reads the create runtime dependency value from a generated app package.json.
51
+ function readGeneratedRuntimeDependency(appDir) {
52
+ // Resolves generated package manifest path.
53
+ const packageJsonPath = path.join(appDir, 'package.json');
54
+ // Parses generated package manifest to inspect runtime dependency pin.
55
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
56
+ return packageJson?.dependencies?.['create-portfoliable'];
57
+ }
58
+
59
+ // Reads the local package version used as default runtime dependency baseline.
60
+ function readLocalPackageVersion() {
61
+ // Resolves local create-portfoliable package manifest.
62
+ const packageJsonPath = path.join(projectRoot, 'package.json');
63
+ // Parses local package manifest and returns semantic version.
64
+ return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')).version;
65
+ }
66
+
67
+ // MARK: SMOKE ORCHESTRATION
68
+ // Generates an app from the local initializer, builds it, and validates emitted artifacts/content.
69
+ function main() {
70
+ console.log('[smoke:init] Preparing temp workspace...');
71
+ fs.rmSync(tempRoot, { recursive: true, force: true });
72
+ fs.mkdirSync(tempRoot, { recursive: true });
73
+
74
+ // Builds expected default runtime dependency from local package version.
75
+ const expectedDefaultRuntimeDep = `^${readLocalPackageVersion()}`;
76
+
77
+ console.log('[smoke:init] Verifying generated default runtime dependency (no override)...');
78
+ runOrFail(
79
+ 'node',
80
+ ['./bin/create-portfoliable.mjs', generatedAppDir, '--no-install', '--no-preview', '--no-interactive'],
81
+ projectRoot
82
+ );
83
+
84
+ // Ensures scaffolded dependency follows package version when no override is provided.
85
+ const generatedDefaultDep = readGeneratedRuntimeDependency(generatedAppDir);
86
+ if (generatedDefaultDep !== expectedDefaultRuntimeDep) {
87
+ fail(
88
+ `Unexpected default runtime dependency: expected ${expectedDefaultRuntimeDep}, got ${String(generatedDefaultDep)}`
89
+ );
90
+ }
91
+
92
+ // Cleans generated app before running existing integration/build path.
93
+ fs.rmSync(generatedAppDir, { recursive: true, force: true });
94
+
95
+ console.log('[smoke:init] Generating app from local initializer...');
96
+ runOrFail(
97
+ 'node',
98
+ ['./bin/create-portfoliable.mjs', generatedAppDir, '--no-preview', '--no-interactive'],
99
+ projectRoot,
100
+ { PORTFOLIABLE_RUNTIME_DEP: `file:${projectRoot}` }
101
+ );
102
+
103
+ console.log('[smoke:init] Building generated app...');
104
+ runOrFail('npm', ['run', 'portfoliable-build'], generatedAppDir);
105
+
106
+ // Resolves generated build assets directory.
107
+ const distAssetsDir = path.join(generatedAppDir, 'dist', 'assets');
108
+ if (!fs.existsSync(distAssetsDir)) {
109
+ fail('dist/assets not found in generated app build output.');
110
+ }
111
+
112
+ // Reads generated asset filenames.
113
+ const assets = fs.readdirSync(distAssetsDir);
114
+
115
+ // Resolves generated main bundle filename.
116
+ const mainBundle = assets.find((file) => file.startsWith('index-') && file.endsWith('.js'));
117
+ if (!mainBundle) {
118
+ fail('Could not find generated app main bundle (index-*.js).');
119
+ }
120
+
121
+ // Loads generated main bundle text for starter content assertions.
122
+ const bundleText = fs.readFileSync(path.join(distAssetsDir, mainBundle), 'utf8');
123
+ ensureContains(bundleText, 'Making Portfoliable', 'starter case title');
124
+ ensureContains(bundleText, 'Apple MacBook Air M5', 'starter model mapping');
125
+
126
+ console.log('[smoke:init] Initializer smoke checks passed.');
127
+ }
128
+
129
+ main();
@@ -0,0 +1,184 @@
1
+ // File: create-portfoliable/scripts/smoke-packed.mjs
2
+ // Purpose: Smoke-test npm-packed initializer artifact by generating and building a consumer app.
3
+ // Author: Lio Schimanko
4
+
5
+ // MARK: IMPORTS
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { spawnSync } from 'node:child_process';
10
+
11
+ // MARK: PATH CONSTANTS
12
+ // Resolves package root used for packing and command execution.
13
+ const projectRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
14
+ // Defines isolated temporary root used for packed-artifact smoke runs.
15
+ const tempRoot = path.join(os.tmpdir(), 'portfoliable-packed-smoke');
16
+ // Defines generated application output path.
17
+ const generatedAppDir = path.join(tempRoot, 'my-portfolio');
18
+
19
+ // MARK: ERROR HANDLING
20
+ // Exits immediately with a prefixed packed-smoke failure message.
21
+ function fail(message) {
22
+ console.error(`[smoke:packed] ${message}`);
23
+ process.exit(1);
24
+ }
25
+
26
+ // MARK: COMMAND EXECUTION HELPERS
27
+ // Runs external commands with inherited IO and fails on non-zero status.
28
+ function runOrFail(command, args, cwd, env = undefined) {
29
+ // Executes child command with inherited terminal output.
30
+ const result = spawnSync(command, args, {
31
+ cwd,
32
+ stdio: 'inherit',
33
+ shell: process.platform === 'win32',
34
+ env: env ? { ...process.env, ...env } : process.env
35
+ });
36
+
37
+ if (result.status !== 0) {
38
+ fail(`Command failed: ${command} ${args.join(' ')}`);
39
+ }
40
+ }
41
+
42
+ // Runs external commands while capturing stdout/stderr, used for JSON-producing commands.
43
+ function runCaptureOrFail(command, args, cwd) {
44
+ // Executes command while capturing stdout/stderr for parser use.
45
+ const result = spawnSync(command, args, {
46
+ cwd,
47
+ stdio: ['ignore', 'pipe', 'pipe'],
48
+ encoding: 'utf8',
49
+ shell: process.platform === 'win32'
50
+ });
51
+
52
+ if (result.status !== 0) {
53
+ // Captures stderr text to enrich failure diagnostics.
54
+ const stderr = (result.stderr || '').trim();
55
+ fail(`Command failed: ${command} ${args.join(' ')}${stderr ? `\n${stderr}` : ''}`);
56
+ }
57
+
58
+ return (result.stdout || '').trim();
59
+ }
60
+
61
+ // MARK: ASSERTION HELPER
62
+ // Verifies the generated bundle contains expected content markers.
63
+ function ensureContains(text, needle, description) {
64
+ if (!text.includes(needle)) {
65
+ fail(`Missing expected marker (${description}): ${needle}`);
66
+ }
67
+ }
68
+
69
+ // Reads create runtime dependency from generated app package.json.
70
+ function readGeneratedRuntimeDependency(appDir) {
71
+ // Resolves generated package manifest path.
72
+ const packageJsonPath = path.join(appDir, 'package.json');
73
+ // Parses generated package manifest to inspect runtime dependency version.
74
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
75
+ return packageJson?.dependencies?.['create-portfoliable'];
76
+ }
77
+
78
+ // Reads local package version used as expected default dependency baseline.
79
+ function readLocalPackageVersion() {
80
+ // Resolves local package manifest.
81
+ const packageJsonPath = path.join(projectRoot, 'package.json');
82
+ // Parses package version string from local manifest.
83
+ return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')).version;
84
+ }
85
+
86
+ // MARK: PACK RESOLUTION
87
+ // Builds an npm tarball and resolves the exact file path returned by npm pack.
88
+ function resolveTarballPath() {
89
+ // Runs npm pack and captures JSON output.
90
+ const packOutput = runCaptureOrFail('npm', ['pack', '--json'], projectRoot);
91
+ let parsed;
92
+
93
+ try {
94
+ parsed = JSON.parse(packOutput);
95
+ } catch {
96
+ fail(`Could not parse npm pack --json output: ${packOutput}`);
97
+ }
98
+
99
+ // Extracts returned tarball filename from npm pack JSON payload.
100
+ const filename = parsed?.[0]?.filename;
101
+ if (!filename) {
102
+ fail('npm pack did not return a tarball filename.');
103
+ }
104
+
105
+ // Resolves tarball absolute path on disk.
106
+ const tarballPath = path.join(projectRoot, filename);
107
+ if (!fs.existsSync(tarballPath)) {
108
+ fail(`Packed tarball not found: ${tarballPath}`);
109
+ }
110
+
111
+ return tarballPath;
112
+ }
113
+
114
+ // MARK: SMOKE ORCHESTRATION
115
+ // Packs the artifact, scaffolds an app from that artifact, builds it, and validates outputs.
116
+ function main() {
117
+ console.log('[smoke:packed] Preparing temp workspace...');
118
+ fs.rmSync(tempRoot, { recursive: true, force: true });
119
+ fs.mkdirSync(tempRoot, { recursive: true });
120
+
121
+ console.log('[smoke:packed] Packing create-portfoliable...');
122
+ // Resolves packed tarball path for installer smoke run.
123
+ const tarballPath = resolveTarballPath();
124
+ // Builds file: dependency string consumed by npm exec package override.
125
+ const runtimeDep = `file:${tarballPath}`;
126
+ // Builds expected default runtime dependency for no-override scaffolding.
127
+ const expectedDefaultRuntimeDep = `^${readLocalPackageVersion()}`;
128
+ // Resolves dedicated output path for default-no-override validation.
129
+ const generatedDefaultAppDir = path.join(tempRoot, 'my-portfolio-default');
130
+
131
+ try {
132
+ console.log('[smoke:packed] Verifying generated default runtime dependency (no override)...');
133
+ runOrFail(
134
+ 'npm',
135
+ ['exec', '--yes', `--package=${runtimeDep}`, 'create', generatedDefaultAppDir, '--', '--no-install', '--no-preview'],
136
+ tempRoot
137
+ );
138
+
139
+ // Ensures packed initializer default dependency tracks packaged version.
140
+ const generatedDefaultDep = readGeneratedRuntimeDependency(generatedDefaultAppDir);
141
+ if (generatedDefaultDep !== expectedDefaultRuntimeDep) {
142
+ fail(
143
+ `Unexpected default runtime dependency: expected ${expectedDefaultRuntimeDep}, got ${String(generatedDefaultDep)}`
144
+ );
145
+ }
146
+
147
+ console.log('[smoke:packed] Generating app from packed artifact...');
148
+ runOrFail(
149
+ 'npm',
150
+ ['exec', '--yes', `--package=${runtimeDep}`, 'create', generatedAppDir, '--', '--no-preview'],
151
+ tempRoot,
152
+ { PORTFOLIABLE_RUNTIME_DEP: runtimeDep }
153
+ );
154
+
155
+ console.log('[smoke:packed] Building generated app...');
156
+ runOrFail('npm', ['run', 'portfoliable-build'], generatedAppDir);
157
+
158
+ // Resolves generated dist assets directory.
159
+ const distAssetsDir = path.join(generatedAppDir, 'dist', 'assets');
160
+ if (!fs.existsSync(distAssetsDir)) {
161
+ fail('dist/assets not found in generated app build output.');
162
+ }
163
+
164
+ // Reads generated asset filenames.
165
+ const assets = fs.readdirSync(distAssetsDir);
166
+
167
+ // Resolves generated main bundle filename.
168
+ const mainBundle = assets.find((file) => file.startsWith('index-') && file.endsWith('.js'));
169
+ if (!mainBundle) {
170
+ fail('Could not find generated app main bundle (index-*.js).');
171
+ }
172
+
173
+ // Loads generated main bundle text for assertions.
174
+ const bundleText = fs.readFileSync(path.join(distAssetsDir, mainBundle), 'utf8');
175
+ ensureContains(bundleText, 'Making Portfoliable', 'starter case title');
176
+ ensureContains(bundleText, 'Apple MacBook Air M5', 'starter model mapping');
177
+
178
+ console.log('[smoke:packed] Packed-artifact smoke checks passed.');
179
+ } finally {
180
+ fs.rmSync(tarballPath, { force: true });
181
+ }
182
+ }
183
+
184
+ main();
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ // File: scripts/switch-valence-source.mjs
3
+ // Purpose: Switch installed valence source between declared npm range and local workspace path.
4
+ // Author: Lio Schimanko
5
+
6
+ // MARK: IMPORTS
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { spawnSync } from 'node:child_process';
10
+
11
+ // MARK: MODE CONFIGURATION
12
+ const VALID_MODES = new Set(['status', 'local', 'npm']);
13
+
14
+ // MARK: PROCESS HELPERS
15
+ // Prints an error and exits immediately for invalid script states.
16
+ function fail(message) {
17
+ console.error(message);
18
+ process.exit(1);
19
+ }
20
+
21
+ // Runs a child process command and fails fast when the command exits non-zero.
22
+ function run(command, args) {
23
+ const result = spawnSync(command, args, { stdio: 'inherit' });
24
+ if (result.status !== 0) {
25
+ fail(`Command failed: ${command} ${args.join(' ')}`);
26
+ }
27
+ }
28
+
29
+ // Reads and parses a JSON file with standardized error reporting.
30
+ function readJson(filePath) {
31
+ try {
32
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
33
+ } catch (error) {
34
+ fail(`Failed to read JSON file: ${filePath}\n${error.message}`);
35
+ }
36
+ }
37
+
38
+ // Reads declared valence dependency range from repository package metadata.
39
+ function getDeclaredRange(repoRoot) {
40
+ const pkg = readJson(path.join(repoRoot, 'package.json'));
41
+ const range = pkg?.dependencies?.['@portfoliablejs/valence'];
42
+ if (!range) {
43
+ fail('Missing dependency declaration for @portfoliablejs/valence in package.json');
44
+ }
45
+ return range;
46
+ }
47
+
48
+ // Detects currently installed valence mode (missing, npm package, or local link).
49
+ function getInstalledState(repoRoot) {
50
+ const modulePath = path.join(repoRoot, 'node_modules', '@portfoliable', 'valence');
51
+
52
+ if (!fs.existsSync(modulePath)) {
53
+ return {
54
+ installed: false,
55
+ mode: 'missing',
56
+ version: null,
57
+ symlinkTarget: null,
58
+ };
59
+ }
60
+
61
+ const stat = fs.lstatSync(modulePath);
62
+ const symlinkTarget = stat.isSymbolicLink() ? fs.readlinkSync(modulePath) : null;
63
+ const packageJsonPath = path.join(modulePath, 'package.json');
64
+ const version = fs.existsSync(packageJsonPath) ? readJson(packageJsonPath).version : null;
65
+
66
+ return {
67
+ installed: true,
68
+ mode: stat.isSymbolicLink() ? 'local-link' : 'npm-package',
69
+ version,
70
+ symlinkTarget,
71
+ };
72
+ }
73
+
74
+ // Warns when required template exports are missing from installed valence source.
75
+ function warnIfTemplateExportsMissing(repoRoot) {
76
+ const mainPath = path.join(repoRoot, 'node_modules', '@portfoliable', 'valence', 'src', 'main.js');
77
+ if (!fs.existsSync(mainPath)) return;
78
+
79
+ const source = fs.readFileSync(mainPath, 'utf8');
80
+ const hasHomeViewExport = source.includes("./stories/templates/HomeView/HomeView.js");
81
+ const hasPlayerViewExport = source.includes("./stories/templates/PlayerView/PlayerView.js");
82
+
83
+ if (hasHomeViewExport && hasPlayerViewExport) return;
84
+
85
+ console.warn('warning: installed @portfoliablejs/valence package is missing template exports required by create-portfoliable runtime (HomeView/PlayerView).');
86
+ console.warn('warning: use npm run valence:local while developing until a published Valence version includes template exports.');
87
+ }
88
+
89
+ // Prints declared dependency and currently installed valence status information.
90
+ function printStatus(repoRoot, declaredRange) {
91
+ const installed = getInstalledState(repoRoot);
92
+
93
+ console.log(`declared range: ${declaredRange}`);
94
+ if (!installed.installed) {
95
+ console.log('installed: no');
96
+ return;
97
+ }
98
+
99
+ console.log(`installed: yes (${installed.mode})`);
100
+ if (installed.version) {
101
+ console.log(`installed version: ${installed.version}`);
102
+ }
103
+ if (installed.symlinkTarget) {
104
+ console.log(`local link target: ${installed.symlinkTarget}`);
105
+ }
106
+
107
+ warnIfTemplateExportsMissing(repoRoot);
108
+ }
109
+
110
+ // MARK: SOURCE SWITCH OPERATIONS
111
+ // Installs valence from a local filesystem path and refreshes compatibility assets.
112
+ function switchToLocal(repoRoot) {
113
+ const localInput = process.env.VALENCE_LOCAL_PATH || '../../valence';
114
+ const localPath = path.resolve(repoRoot, localInput);
115
+ const localPackageJsonPath = path.join(localPath, 'package.json');
116
+
117
+ if (!fs.existsSync(localPackageJsonPath)) {
118
+ fail(`Could not find valence package.json at: ${localPackageJsonPath}`);
119
+ }
120
+
121
+ const localPkg = readJson(localPackageJsonPath);
122
+ if (localPkg.name !== '@portfoliablejs/valence') {
123
+ fail(`Expected package name @portfoliablejs/valence at ${localPackageJsonPath}, found ${localPkg.name || '(missing)'}`);
124
+ }
125
+
126
+ run('npm', ['install', `@portfoliablejs/valence@file:${localPath}`, '--no-save']);
127
+ run('npm', ['run', 'ensure:valence']);
128
+ }
129
+
130
+ // Installs valence from the declared npm range and refreshes compatibility assets.
131
+ function switchToNpm(declaredRange) {
132
+ run('npm', ['install', `@portfoliablejs/valence@${declaredRange}`, '--no-save']);
133
+ run('npm', ['run', 'ensure:valence']);
134
+ }
135
+
136
+ // MARK: SCRIPT ENTRYPOINT
137
+ // Routes CLI mode selection for status reporting and source switching.
138
+ function main() {
139
+ const mode = process.argv[2] || 'status';
140
+ if (!VALID_MODES.has(mode)) {
141
+ fail('Usage: node ./scripts/switch-valence-source.mjs [status|local|npm]');
142
+ }
143
+
144
+ const repoRoot = process.cwd();
145
+ const declaredRange = getDeclaredRange(repoRoot);
146
+
147
+ if (mode === 'status') {
148
+ printStatus(repoRoot, declaredRange);
149
+ return;
150
+ }
151
+
152
+ if (mode === 'local') {
153
+ switchToLocal(repoRoot);
154
+ printStatus(repoRoot, declaredRange);
155
+ return;
156
+ }
157
+
158
+ switchToNpm(declaredRange);
159
+ printStatus(repoRoot, declaredRange);
160
+ }
161
+
162
+ main();
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ // File: scripts/sync-template-shared.mjs
3
+ // Purpose: Keep selected initializer template files synchronized with canonical root sources.
4
+ // Author: Lio Schimanko
5
+
6
+ // MARK: IMPORTS
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ // MARK: PATH CONSTANTS
12
+ // Resolves this script directory path.
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+ // Resolves package root used to resolve mapped source/target files.
15
+ const repoRoot = path.resolve(__dirname, '..');
16
+
17
+ // MARK: TEMPLATE HEADER
18
+ // Defines template parser header injected into synchronized parser target file.
19
+ const TEMPLATE_PARSER_HEADER = [
20
+ '// File: create-portfoliable/templates/src/parser/markdown.js',
21
+ '// Purpose: Parse template markdown cases into normalized case data.',
22
+ '// Author: Lio Schimanko',
23
+ '// Note: Auto-synced from src/parser/markdown.js. Edit the canonical source only.',
24
+ ''
25
+ ].join('\n');
26
+
27
+ // MARK: SYNC MAPPINGS
28
+ // Lists source/target mapping definitions and optional transform functions.
29
+ const mappings = [
30
+ {
31
+ source: 'src/parser/markdown.js',
32
+ target: 'templates/src/parser/markdown.js',
33
+ // Removes source header and injects template-specific header contract.
34
+ transform: (sourceText) => {
35
+ // Splits source into lines for header detection.
36
+ const lines = sourceText.split('\n');
37
+ // Tracks start index for parser body after optional 3-line header.
38
+ let bodyStart = 0;
39
+
40
+ if (
41
+ lines[0]?.startsWith('// File:')
42
+ && lines[1]?.startsWith('// Purpose:')
43
+ && lines[2]?.startsWith('// Author:')
44
+ ) {
45
+ bodyStart = lines[3] === '' ? 4 : 3;
46
+ }
47
+
48
+ // Extracts parser body and strips leading blank lines.
49
+ const body = lines.slice(bodyStart).join('\n').replace(/^\n+/, '');
50
+ return `${TEMPLATE_PARSER_HEADER}${body}`;
51
+ }
52
+ }
53
+ ];
54
+
55
+ // Resolves repository-relative path to absolute path.
56
+ function resolveRepoPath(relPath) {
57
+ return path.resolve(repoRoot, relPath);
58
+ }
59
+
60
+ // Reads UTF-8 file content.
61
+ function readUtf8(filePath) {
62
+ return fs.readFileSync(filePath, 'utf8');
63
+ }
64
+
65
+ // Writes UTF-8 file content, creating parent directories when needed.
66
+ function writeUtf8(filePath, content) {
67
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
68
+ fs.writeFileSync(filePath, content, 'utf8');
69
+ }
70
+
71
+ // Normalizes EOL format to LF for reliable cross-platform comparisons.
72
+ function normalizeEol(text) {
73
+ return text.replace(/\r\n/g, '\n');
74
+ }
75
+
76
+ // Executes one mapping sync action in check or write mode.
77
+ function syncOne(mapping, mode) {
78
+ // Resolves mapped source path.
79
+ const sourcePath = resolveRepoPath(mapping.source);
80
+ // Resolves mapped target path.
81
+ const targetPath = resolveRepoPath(mapping.target);
82
+ // Reads source file text.
83
+ const sourceText = readUtf8(sourcePath);
84
+ // Applies optional transform to source text.
85
+ const expectedText = mapping.transform ? mapping.transform(sourceText) : sourceText;
86
+ // Normalizes expected text for comparison.
87
+ const expectedNormalized = normalizeEol(expectedText);
88
+
89
+ // Reads current target file text when present.
90
+ const currentText = fs.existsSync(targetPath) ? readUtf8(targetPath) : '';
91
+ // Normalizes current target text for comparison.
92
+ const currentNormalized = normalizeEol(currentText);
93
+ // Determines whether target file already matches expected content.
94
+ const isSynced = expectedNormalized === currentNormalized;
95
+
96
+ if (mode === 'check') {
97
+ if (!isSynced) {
98
+ console.error(`[sync:templates] Drift detected: ${mapping.target}`);
99
+ console.error(`[sync:templates] Canonical source: ${mapping.source}`);
100
+ return false;
101
+ }
102
+ console.log(`[sync:templates] OK: ${mapping.target}`);
103
+ return true;
104
+ }
105
+
106
+ if (!isSynced) {
107
+ writeUtf8(targetPath, expectedText.endsWith('\n') ? expectedText : `${expectedText}\n`);
108
+ console.log(`[sync:templates] Updated: ${mapping.target}`);
109
+ } else {
110
+ console.log(`[sync:templates] Unchanged: ${mapping.target}`);
111
+ }
112
+
113
+ return true;
114
+ }
115
+
116
+ // MARK: SCRIPT ENTRYPOINT
117
+ // Runs all mappings in check or write mode and exits non-zero on drift.
118
+ function main() {
119
+ // Parses CLI args used to switch between check and write mode.
120
+ const args = new Set(process.argv.slice(2));
121
+ // Resolves operating mode from CLI flags.
122
+ const mode = args.has('--check') ? 'check' : 'write';
123
+
124
+ // Runs sync/check for each configured mapping.
125
+ const results = mappings.map((mapping) => syncOne(mapping, mode));
126
+ // Aggregates overall success result.
127
+ const ok = results.every(Boolean);
128
+
129
+ if (!ok) {
130
+ console.error('[sync:templates] Template files are out of sync. Run: npm run sync:templates');
131
+ process.exit(1);
132
+ }
133
+ }
134
+
135
+ // Executes sync script main function.
136
+ main();