morphisms 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +45 -0
- package/bin/mor-workbench.js +3 -0
- package/bin/mor.js +3 -0
- package/bin/morphisms.js +9 -0
- package/lib/cli.js +28 -0
- package/lib/files.js +49 -0
- package/lib/forward.js +45 -0
- package/lib/init.js +190 -0
- package/lib/platform.js +40 -0
- package/lib/update.js +45 -0
- package/lib/workbench.js +120 -0
- package/package.json +24 -0
- package/templates/README.md +21 -0
- package/templates/agent/.gitkeep +0 -0
- package/templates/agent/skills/morphisms/SKILL.md +86 -0
- package/templates/claude/.gitkeep +0 -0
- package/templates/claude/skills/morphisms/SKILL.md +86 -0
- package/templates/contract/README.md +57 -0
- package/templates/contract/contract.mor +247 -0
- package/templates/contract/personas/investigator.md +20 -0
- package/templates/contract/personas/operator-facing-intake.md +16 -0
- package/templates/contract/personas/plan-reviewer.md +20 -0
- package/templates/contract/personas/scoper.md +23 -0
- package/templates/contract/personas/triager.md +22 -0
- package/templates/contract/schemas/investigation-report.schema.json +28 -0
- package/templates/contract/schemas/operator-decision.schema.json +13 -0
- package/templates/contract/schemas/plan-review.schema.json +26 -0
- package/templates/contract/schemas/scoped-plan.schema.json +25 -0
- package/templates/contract/schemas/triage-decision.schema.json +25 -0
- package/templates/instructions/starter-issues-agent-manual.md +232 -0
- package/templates/instructions/workspace-agent-manual.md +22 -0
- package/templates/manifest.json +22 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Morphisms Proprietary Preview License
|
|
2
|
+
|
|
3
|
+
Licensor: Blake Sims / Zen AI Tutoring
|
|
4
|
+
Software: Morphisms preview binaries and packaged materials
|
|
5
|
+
|
|
6
|
+
1. Preview grant. You may install and use the Software locally for personal use,
|
|
7
|
+
internal business use, and commercial evaluation.
|
|
8
|
+
|
|
9
|
+
2. No redistribution. You may not copy, sell, sublicense, republish, mirror,
|
|
10
|
+
upload, package, or otherwise redistribute the Software or its binaries to
|
|
11
|
+
any third party without written permission from the Licensor.
|
|
12
|
+
|
|
13
|
+
3. No reverse engineering. You may not reverse engineer, decompile, disassemble,
|
|
14
|
+
modify, bypass technical limits in, or create derivative binary builds of the
|
|
15
|
+
Software, except where applicable law expressly forbids this restriction.
|
|
16
|
+
|
|
17
|
+
4. No hosted service resale. You may not provide the Software as a hosted or
|
|
18
|
+
managed service for others, or use it to offer substantially similar workflow
|
|
19
|
+
runtime services, without written permission from the Licensor.
|
|
20
|
+
|
|
21
|
+
5. Ownership. The Software is licensed, not sold. The Licensor retains all right,
|
|
22
|
+
title, and interest in the Software and related intellectual property.
|
|
23
|
+
|
|
24
|
+
6. Preview software. The Software is a preview. It may change, break, lose
|
|
25
|
+
compatibility, be withdrawn, or stop working at any time. Features, APIs,
|
|
26
|
+
data formats, and availability are not guaranteed.
|
|
27
|
+
|
|
28
|
+
7. Feedback. If you provide feedback, suggestions, or bug reports, you grant the
|
|
29
|
+
Licensor permission to use them without restriction or compensation.
|
|
30
|
+
|
|
31
|
+
8. Termination. This license terminates automatically if you violate its terms.
|
|
32
|
+
After termination, you must stop using the Software and delete all copies in
|
|
33
|
+
your possession or control.
|
|
34
|
+
|
|
35
|
+
9. No warranty. THE SOFTWARE IS PROVIDED "AS IS" AND "AS AVAILABLE," WITHOUT
|
|
36
|
+
WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF
|
|
37
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND
|
|
38
|
+
NON-INFRINGEMENT.
|
|
39
|
+
|
|
40
|
+
10. Limitation of liability. TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE
|
|
41
|
+
LICENSOR WILL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL,
|
|
42
|
+
CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES, OR FOR LOST PROFITS, LOST
|
|
43
|
+
DATA, BUSINESS INTERRUPTION, OR PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES.
|
|
44
|
+
|
|
45
|
+
If you want rights not granted here, contact Blake Sims / Zen AI Tutoring.
|
package/bin/mor.js
ADDED
package/bin/morphisms.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { main } = require('../lib/cli');
|
|
3
|
+
|
|
4
|
+
Promise.resolve(main(process.argv.slice(2))).then((code) => {
|
|
5
|
+
if (typeof code === 'number') process.exit(code);
|
|
6
|
+
}).catch((error) => {
|
|
7
|
+
console.error(error && error.stack ? error.stack : String(error));
|
|
8
|
+
process.exit(1);
|
|
9
|
+
});
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const pkg = require('../package.json');
|
|
2
|
+
const { initCommand } = require('./init');
|
|
3
|
+
const { updateCommand } = require('./update');
|
|
4
|
+
const { workbenchCommand } = require('./workbench');
|
|
5
|
+
|
|
6
|
+
function help() {
|
|
7
|
+
console.log(`Morphisms ${pkg.version}\n\nUsage:\n morphisms init\n morphisms workbench [--port N]\n morphisms update\n morphisms --version`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function main(argv) {
|
|
11
|
+
const [command, ...rest] = argv;
|
|
12
|
+
if (!command || command === '-h' || command === '--help') {
|
|
13
|
+
help();
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
if (command === '-v' || command === '--version' || command === 'version') {
|
|
17
|
+
console.log(pkg.version);
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
if (command === 'init') return initCommand(rest);
|
|
21
|
+
if (command === 'update') return updateCommand(rest);
|
|
22
|
+
if (command === 'workbench') return workbenchCommand(rest);
|
|
23
|
+
console.error(`unknown morphisms command: ${command}`);
|
|
24
|
+
help();
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { main };
|
package/lib/files.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
function exists(filePath) {
|
|
5
|
+
return fs.existsSync(filePath);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function ensureDir(dirPath) {
|
|
9
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function copyFileNoClobber(src, dest, notes) {
|
|
13
|
+
if (exists(dest)) {
|
|
14
|
+
notes.push(`skip existing ${dest}`);
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
ensureDir(path.dirname(dest));
|
|
18
|
+
fs.copyFileSync(src, dest);
|
|
19
|
+
notes.push(`wrote ${dest}`);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function copyTreeNoClobber(srcDir, destDir, notes) {
|
|
24
|
+
if (!exists(srcDir)) {
|
|
25
|
+
throw new Error(`template directory is missing: ${srcDir}`);
|
|
26
|
+
}
|
|
27
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
28
|
+
const src = path.join(srcDir, entry.name);
|
|
29
|
+
const dest = path.join(destDir, entry.name);
|
|
30
|
+
if (entry.isDirectory()) {
|
|
31
|
+
copyTreeNoClobber(src, dest, notes);
|
|
32
|
+
} else if (entry.isFile()) {
|
|
33
|
+
copyFileNoClobber(src, dest, notes);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeJsonNoClobber(dest, value, notes) {
|
|
39
|
+
if (exists(dest)) {
|
|
40
|
+
notes.push(`skip existing ${dest}`);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
ensureDir(path.dirname(dest));
|
|
44
|
+
fs.writeFileSync(dest, `${JSON.stringify(value, null, 2)}\n`);
|
|
45
|
+
notes.push(`wrote ${dest}`);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { copyFileNoClobber, copyTreeNoClobber, ensureDir, exists, writeJsonNoClobber };
|
package/lib/forward.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
2
|
+
const { printPlatformError, resolvePlatformBinary } = require('./platform');
|
|
3
|
+
|
|
4
|
+
function forwardSync(binaryName, argv) {
|
|
5
|
+
let binaryPath;
|
|
6
|
+
try {
|
|
7
|
+
binaryPath = resolvePlatformBinary(binaryName);
|
|
8
|
+
} catch (error) {
|
|
9
|
+
printPlatformError(error);
|
|
10
|
+
return 1;
|
|
11
|
+
}
|
|
12
|
+
const result = spawnSync(binaryPath, argv, { stdio: 'inherit' });
|
|
13
|
+
if (result.error) {
|
|
14
|
+
console.error(`failed to run ${binaryName}: ${result.error.message}`);
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
if (result.signal) {
|
|
18
|
+
return 1;
|
|
19
|
+
}
|
|
20
|
+
return typeof result.status === 'number' ? result.status : 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function forwardSpawn(binaryName, argv) {
|
|
24
|
+
let binaryPath;
|
|
25
|
+
try {
|
|
26
|
+
binaryPath = resolvePlatformBinary(binaryName);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
printPlatformError(error);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const child = spawn(binaryPath, argv, { stdio: 'inherit' });
|
|
32
|
+
child.on('error', (error) => {
|
|
33
|
+
console.error(`failed to run ${binaryName}: ${error.message}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
|
36
|
+
child.on('exit', (code, signal) => {
|
|
37
|
+
if (signal) {
|
|
38
|
+
process.kill(process.pid, signal);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
process.exit(typeof code === 'number' ? code : 1);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { forwardSpawn, forwardSync };
|
package/lib/init.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
const { spawnSync } = require('node:child_process');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const readline = require('node:readline');
|
|
5
|
+
const { copyFileNoClobber, copyTreeNoClobber, ensureDir, exists, writeJsonNoClobber } = require('./files');
|
|
6
|
+
|
|
7
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
8
|
+
const TEMPLATES_ROOT = path.join(PACKAGE_ROOT, 'templates');
|
|
9
|
+
const CONTRACT_TEMPLATE_DIR = path.join(TEMPLATES_ROOT, 'contract');
|
|
10
|
+
const DEFAULT_PORT = 4400;
|
|
11
|
+
const DISTRO_ALIAS = 'issues';
|
|
12
|
+
const VIEWER_REGISTRY_GITIGNORE_ENTRY = '.morphisms-contract/viewer-workspaces.json';
|
|
13
|
+
|
|
14
|
+
function resolveContractSource(repoRoot, contractDir) {
|
|
15
|
+
if (exists(path.join(contractDir, 'distro.yaml'))) return path.relative(repoRoot, contractDir) || '.';
|
|
16
|
+
const morFile = path.join(contractDir, 'contract.mor');
|
|
17
|
+
if (exists(morFile)) return path.relative(repoRoot, morFile);
|
|
18
|
+
return path.relative(repoRoot, contractDir) || '.';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createPrompt() {
|
|
22
|
+
if (!process.stdin.isTTY) {
|
|
23
|
+
const answers = fs.readFileSync(0, 'utf8').split(/\r?\n/);
|
|
24
|
+
let index = 0;
|
|
25
|
+
return {
|
|
26
|
+
question(text) {
|
|
27
|
+
process.stdout.write(text);
|
|
28
|
+
return Promise.resolve((answers[index++] || '').trim());
|
|
29
|
+
},
|
|
30
|
+
close() {},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
34
|
+
return {
|
|
35
|
+
question(text) {
|
|
36
|
+
return new Promise((resolve) => rl.question(text, (answer) => resolve(answer.trim())));
|
|
37
|
+
},
|
|
38
|
+
close() {
|
|
39
|
+
rl.close();
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function askYesNo(prompt, label, defaultValue) {
|
|
45
|
+
const suffix = defaultValue ? '[Y/n]' : '[y/N]';
|
|
46
|
+
const answer = (await prompt.question(`${label} ${suffix} `)).toLowerCase();
|
|
47
|
+
if (!answer) return defaultValue;
|
|
48
|
+
return answer === 'y' || answer === 'yes';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function askPort(prompt) {
|
|
52
|
+
for (;;) {
|
|
53
|
+
const answer = await prompt.question(`Workbench port [${DEFAULT_PORT}] `);
|
|
54
|
+
if (!answer) return DEFAULT_PORT;
|
|
55
|
+
const port = Number(answer);
|
|
56
|
+
if (Number.isInteger(port) && port > 0 && port < 65536) return port;
|
|
57
|
+
console.log('Please enter a TCP port from 1 to 65535.');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runMor(args, cwd, extraEnv = {}) {
|
|
62
|
+
console.log(`$ mor ${args.join(' ')}`);
|
|
63
|
+
const result = spawnSync('mor', args, { cwd, stdio: 'inherit', env: { ...process.env, ...extraEnv } });
|
|
64
|
+
if (result.error) {
|
|
65
|
+
throw new Error(`failed to run mor ${args.join(' ')}: ${result.error.message}`);
|
|
66
|
+
}
|
|
67
|
+
if (result.status !== 0) {
|
|
68
|
+
throw new Error(`mor ${args.join(' ')} exited with ${result.status}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function ensureGitignoreEntry(repoRoot, entry, notes) {
|
|
73
|
+
const gitignorePath = path.join(repoRoot, '.gitignore');
|
|
74
|
+
const existing = exists(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
|
|
75
|
+
const hasEntry = existing.split(/\r?\n/).some((line) => line.trim() === entry);
|
|
76
|
+
if (hasEntry) {
|
|
77
|
+
notes.push(`gitignore already contains ${entry}`);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
|
|
81
|
+
const header = existing.includes('# Morphisms local machine state') ? '' : '# Morphisms local machine state\n';
|
|
82
|
+
fs.writeFileSync(gitignorePath, `${existing}${prefix}${header}${entry}\n`);
|
|
83
|
+
notes.push(`added ${entry} to .gitignore`);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function installManifest(repoRoot, flavors, notes) {
|
|
88
|
+
const manifestPath = path.join(TEMPLATES_ROOT, 'manifest.json');
|
|
89
|
+
if (!exists(manifestPath)) {
|
|
90
|
+
notes.push('skip missing templates/manifest.json');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
94
|
+
if (!Array.isArray(manifest)) {
|
|
95
|
+
throw new Error('templates/manifest.json must be an array');
|
|
96
|
+
}
|
|
97
|
+
for (const entry of manifest) {
|
|
98
|
+
validateManifestEntry(entry);
|
|
99
|
+
if (entry.when === 'claude' && !flavors.claude) continue;
|
|
100
|
+
if (entry.when === 'agent' && !flavors.agent) continue;
|
|
101
|
+
const src = path.join(TEMPLATES_ROOT, entry.src);
|
|
102
|
+
const dest = resolveManifestDest(repoRoot, entry.dest);
|
|
103
|
+
if (!path.resolve(src).startsWith(TEMPLATES_ROOT + path.sep)) {
|
|
104
|
+
throw new Error(`manifest src escapes templates/: ${entry.src}`);
|
|
105
|
+
}
|
|
106
|
+
if (!exists(src)) {
|
|
107
|
+
notes.push(`skip missing template ${entry.src}`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const stat = fs.statSync(src);
|
|
111
|
+
if (stat.isDirectory()) {
|
|
112
|
+
copyTreeNoClobber(src, dest, notes);
|
|
113
|
+
} else if (stat.isFile()) {
|
|
114
|
+
copyFileNoClobber(src, dest, notes);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function validateManifestEntry(entry) {
|
|
120
|
+
if (!entry || typeof entry !== 'object') throw new Error('manifest entries must be objects');
|
|
121
|
+
if (typeof entry.src !== 'string' || entry.src.includes('..') || path.isAbsolute(entry.src)) {
|
|
122
|
+
throw new Error(`invalid manifest src: ${entry.src}`);
|
|
123
|
+
}
|
|
124
|
+
if (typeof entry.dest !== 'string' || !entry.dest.includes('{repo}')) {
|
|
125
|
+
throw new Error(`invalid manifest dest: ${entry.dest}`);
|
|
126
|
+
}
|
|
127
|
+
if (!['claude', 'agent', 'always'].includes(entry.when)) {
|
|
128
|
+
throw new Error(`invalid manifest when: ${entry.when}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolveManifestDest(repoRoot, destTemplate) {
|
|
133
|
+
const resolvedRepo = path.resolve(repoRoot);
|
|
134
|
+
const replaced = destTemplate.replaceAll('{repo}', resolvedRepo);
|
|
135
|
+
const resolvedDest = path.resolve(replaced);
|
|
136
|
+
if (resolvedDest !== resolvedRepo && !resolvedDest.startsWith(resolvedRepo + path.sep)) {
|
|
137
|
+
throw new Error(`manifest dest escapes repository: ${destTemplate}`);
|
|
138
|
+
}
|
|
139
|
+
return resolvedDest;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function initCommand() {
|
|
143
|
+
const repoRoot = path.resolve(process.cwd());
|
|
144
|
+
const detectedClaude = exists(path.join(repoRoot, '.claude'));
|
|
145
|
+
const detectedAgent = exists(path.join(repoRoot, '.agent'));
|
|
146
|
+
const prompt = createPrompt();
|
|
147
|
+
const notes = [];
|
|
148
|
+
try {
|
|
149
|
+
console.log('Morphisms init will install the starter contract and local agent-facing files.');
|
|
150
|
+
const claudeDefault = detectedClaude || !detectedAgent;
|
|
151
|
+
const agentDefault = detectedAgent;
|
|
152
|
+
const claude = await askYesNo(prompt, `Install Claude Code files${detectedClaude ? ' (detected .claude/)' : ''}?`, claudeDefault);
|
|
153
|
+
const agent = await askYesNo(prompt, `Install .agent files${detectedAgent ? ' (detected .agent/)' : ''}?`, agentDefault);
|
|
154
|
+
const port = await askPort(prompt);
|
|
155
|
+
|
|
156
|
+
const contractDest = path.join(repoRoot, '.morphisms-contract', 'issues_front_half');
|
|
157
|
+
copyTreeNoClobber(CONTRACT_TEMPLATE_DIR, contractDest, notes);
|
|
158
|
+
const contractSource = resolveContractSource(repoRoot, contractDest);
|
|
159
|
+
|
|
160
|
+
const viewerRegistry = path.join(repoRoot, '.morphisms-contract', 'viewer-workspaces.json');
|
|
161
|
+
const registryEnv = { MOR_VIEWER_REGISTRY_PATH: viewerRegistry };
|
|
162
|
+
runMor(['setup', contractSource, '--workspace', '.', '--alias', DISTRO_ALIAS, '--activate'], repoRoot, registryEnv);
|
|
163
|
+
runMor(['viewer', 'register', '.'], repoRoot, registryEnv);
|
|
164
|
+
ensureGitignoreEntry(repoRoot, VIEWER_REGISTRY_GITIGNORE_ENTRY, notes);
|
|
165
|
+
runMor(['runtime', 'verify'], repoRoot);
|
|
166
|
+
|
|
167
|
+
installManifest(repoRoot, { claude, agent }, notes);
|
|
168
|
+
writeJsonNoClobber(path.join(repoRoot, '.morphisms-contract', 'config.json'), {
|
|
169
|
+
schema_version: 'morphisms.init-config/1',
|
|
170
|
+
workbench_port: port,
|
|
171
|
+
contract: contractSource,
|
|
172
|
+
distro_alias: DISTRO_ALIAS,
|
|
173
|
+
store_alias: 'issue',
|
|
174
|
+
runtime_db: `.mor/${DISTRO_ALIAS}.sqlite3`,
|
|
175
|
+
viewer_registry: '.morphisms-contract/viewer-workspaces.json',
|
|
176
|
+
agent_flavors: { claude, agent },
|
|
177
|
+
}, notes);
|
|
178
|
+
|
|
179
|
+
for (const note of notes) console.log(note);
|
|
180
|
+
console.log('Next step: open Claude Code here and say: "start morphisms onboarding".');
|
|
181
|
+
return 0;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.error(`morphisms init failed: ${error.message}`);
|
|
184
|
+
return 1;
|
|
185
|
+
} finally {
|
|
186
|
+
prompt.close();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { ensureGitignoreEntry, initCommand, installManifest, resolveContractSource, resolveManifestDest, validateManifestEntry };
|
package/lib/platform.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const SUPPORTED_PLATFORMS = [
|
|
2
|
+
{ platform: 'linux', arch: 'x64', packageName: '@morphisms/bin-linux-x64' },
|
|
3
|
+
{ platform: 'darwin', arch: 'arm64', packageName: '@morphisms/bin-darwin-arm64' },
|
|
4
|
+
{ platform: 'darwin', arch: 'x64', packageName: '@morphisms/bin-darwin-x64' },
|
|
5
|
+
];
|
|
6
|
+
|
|
7
|
+
function platformPackageName(platform = process.platform, arch = process.arch) {
|
|
8
|
+
const match = SUPPORTED_PLATFORMS.find((entry) => entry.platform === platform && entry.arch === arch);
|
|
9
|
+
return match ? match.packageName : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function supportedList() {
|
|
13
|
+
return SUPPORTED_PLATFORMS.map((entry) => `${entry.platform}-${entry.arch} (${entry.packageName})`).join(', ');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolvePlatformBinary(binaryName) {
|
|
17
|
+
const packageName = platformPackageName();
|
|
18
|
+
if (!packageName) {
|
|
19
|
+
throw new Error(`Morphisms does not ship a binary package for ${process.platform}-${process.arch}. Supported platforms: ${supportedList()}.`);
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return require.resolve(`${packageName}/bin/${binaryName}`);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
const wrapped = new Error(`Morphisms could not find the optional platform package ${packageName} for ${process.platform}-${process.arch}. Reinstall with optional dependencies enabled (npm install -g morphisms) or use one of the supported platforms: ${supportedList()}.`);
|
|
25
|
+
wrapped.cause = error;
|
|
26
|
+
throw wrapped;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function printPlatformError(error) {
|
|
31
|
+
console.error(error && error.message ? error.message : String(error));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
SUPPORTED_PLATFORMS,
|
|
36
|
+
platformPackageName,
|
|
37
|
+
printPlatformError,
|
|
38
|
+
resolvePlatformBinary,
|
|
39
|
+
supportedList,
|
|
40
|
+
};
|
package/lib/update.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const https = require('node:https');
|
|
2
|
+
const pkg = require('../package.json');
|
|
3
|
+
|
|
4
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/morphisms';
|
|
5
|
+
|
|
6
|
+
function fetchRegistry() {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const req = https.get(REGISTRY_URL, { headers: { accept: 'application/json' }, timeout: 5000 }, (res) => {
|
|
9
|
+
if (res.statusCode !== 200) {
|
|
10
|
+
res.resume();
|
|
11
|
+
reject(new Error(`registry returned HTTP ${res.statusCode}`));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
let body = '';
|
|
15
|
+
res.setEncoding('utf8');
|
|
16
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
17
|
+
res.on('end', () => {
|
|
18
|
+
try {
|
|
19
|
+
resolve(JSON.parse(body));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
reject(error);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
req.on('timeout', () => req.destroy(new Error('registry request timed out')));
|
|
26
|
+
req.on('error', reject);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function updateCommand() {
|
|
31
|
+
try {
|
|
32
|
+
const data = await fetchRegistry();
|
|
33
|
+
const tags = data['dist-tags'] || {};
|
|
34
|
+
const latest = tags.latest || 'unknown';
|
|
35
|
+
const beta = tags.beta || 'unknown';
|
|
36
|
+
const installed = pkg.version;
|
|
37
|
+
const hint = installed === latest || installed === beta ? 'up to date for a published tag' : 'newer version may be available';
|
|
38
|
+
console.log(`morphisms ${installed} installed; npm latest ${latest}; beta ${beta}; ${hint}.`);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.log(`morphisms ${pkg.version} installed; update check unavailable (${error.message}).`);
|
|
41
|
+
}
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { REGISTRY_URL, updateCommand };
|
package/lib/workbench.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const { spawn } = require('node:child_process');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { exists } = require('./files');
|
|
4
|
+
|
|
5
|
+
const WORKBENCH_LAUNCH = {
|
|
6
|
+
source: '.morphisms-contract/issues_front_half/contract.mor',
|
|
7
|
+
host: '127.0.0.1',
|
|
8
|
+
runtimeDb: '.mor/issues.sqlite3',
|
|
9
|
+
workspaceRegistry: '.morphisms-contract/viewer-workspaces.json',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function parseWorkbenchArgs(argv) {
|
|
13
|
+
let port;
|
|
14
|
+
let workspaceRegistry = process.env.MORPHISMS_WORKBENCH_REGISTRY || undefined;
|
|
15
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
16
|
+
const arg = argv[i];
|
|
17
|
+
if (arg === '--port') {
|
|
18
|
+
const value = argv[++i];
|
|
19
|
+
if (!value) throw new Error('--port requires a value');
|
|
20
|
+
port = parsePort(value);
|
|
21
|
+
} else if (arg.startsWith('--port=')) {
|
|
22
|
+
port = parsePort(arg.slice('--port='.length));
|
|
23
|
+
} else if (arg === '--workspace-registry') {
|
|
24
|
+
workspaceRegistry = argv[++i];
|
|
25
|
+
if (!workspaceRegistry) throw new Error('--workspace-registry requires a value');
|
|
26
|
+
} else if (arg.startsWith('--workspace-registry=')) {
|
|
27
|
+
workspaceRegistry = arg.slice('--workspace-registry='.length);
|
|
28
|
+
if (!workspaceRegistry) throw new Error('--workspace-registry requires a value');
|
|
29
|
+
} else {
|
|
30
|
+
throw new Error(`unknown workbench argument: ${arg}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { port, workspaceRegistry };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parsePort(value) {
|
|
37
|
+
const port = Number(value);
|
|
38
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
39
|
+
throw new Error(`invalid port: ${value}`);
|
|
40
|
+
}
|
|
41
|
+
return port;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readConfig(repoRoot) {
|
|
45
|
+
const configPath = path.join(repoRoot, '.morphisms-contract', 'config.json');
|
|
46
|
+
if (!exists(configPath)) return {};
|
|
47
|
+
try {
|
|
48
|
+
delete require.cache[require.resolve(configPath)];
|
|
49
|
+
return require(configPath);
|
|
50
|
+
} catch (_) {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function configuredPort(repoRoot) {
|
|
56
|
+
const config = readConfig(repoRoot);
|
|
57
|
+
if (!config.workbench_port) return 4400;
|
|
58
|
+
try {
|
|
59
|
+
return parsePort(String(config.workbench_port));
|
|
60
|
+
} catch (_) {
|
|
61
|
+
return 4400;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function configuredSource(repoRoot) {
|
|
66
|
+
const config = readConfig(repoRoot);
|
|
67
|
+
return typeof config.contract === 'string' && config.contract ? config.contract : WORKBENCH_LAUNCH.source;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function configuredRegistry(repoRoot) {
|
|
71
|
+
const config = readConfig(repoRoot);
|
|
72
|
+
return typeof config.viewer_registry === 'string' && config.viewer_registry ? config.viewer_registry : WORKBENCH_LAUNCH.workspaceRegistry;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function configuredRuntimeDb(repoRoot) {
|
|
76
|
+
const config = readConfig(repoRoot);
|
|
77
|
+
return typeof config.runtime_db === 'string' && config.runtime_db ? config.runtime_db : WORKBENCH_LAUNCH.runtimeDb;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildWorkbenchArgv(repoRoot, options = {}) {
|
|
81
|
+
const launchPort = options.port || configuredPort(repoRoot);
|
|
82
|
+
const workspaceRegistry = options.workspaceRegistry || configuredRegistry(repoRoot);
|
|
83
|
+
return [
|
|
84
|
+
configuredSource(repoRoot),
|
|
85
|
+
'--serve',
|
|
86
|
+
`${WORKBENCH_LAUNCH.host}:${launchPort}`,
|
|
87
|
+
'--runtime-db',
|
|
88
|
+
configuredRuntimeDb(repoRoot),
|
|
89
|
+
'--workspace-registry',
|
|
90
|
+
workspaceRegistry,
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function workbenchCommand(argv) {
|
|
95
|
+
let parsed;
|
|
96
|
+
try {
|
|
97
|
+
parsed = parseWorkbenchArgs(argv);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error(error.message);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
const port = parsed.port || configuredPort(process.cwd());
|
|
103
|
+
const childArgv = buildWorkbenchArgv(process.cwd(), parsed);
|
|
104
|
+
console.log(`Starting Morphisms Workbench at http://127.0.0.1:${port}/#workbench`);
|
|
105
|
+
const child = spawn('mor-workbench', childArgv, { stdio: 'inherit' });
|
|
106
|
+
child.on('error', (error) => {
|
|
107
|
+
console.error(`failed to run mor-workbench: ${error.message}`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
});
|
|
110
|
+
child.on('exit', (code, signal) => {
|
|
111
|
+
if (signal) {
|
|
112
|
+
process.kill(process.pid, signal);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
process.exit(typeof code === 'number' ? code : 1);
|
|
116
|
+
});
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { WORKBENCH_LAUNCH, buildWorkbenchArgv, parseWorkbenchArgs, workbenchCommand };
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "morphisms",
|
|
3
|
+
"version": "0.1.0-beta.1",
|
|
4
|
+
"description": "Morphisms workflow runtime shim and onboarding wizard",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"bin": {
|
|
7
|
+
"morphisms": "bin/morphisms.js",
|
|
8
|
+
"mor": "bin/mor.js",
|
|
9
|
+
"mor-workbench": "bin/mor-workbench.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin/",
|
|
13
|
+
"lib/",
|
|
14
|
+
"templates/",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"package.json"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"optionalDependencies": {
|
|
22
|
+
"@morphisms/bin-linux-x64": "0.1.0-beta.1"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Morphisms npm templates
|
|
2
|
+
|
|
3
|
+
This directory is installed by the `morphisms` npm shim and consumed by `morphisms init`.
|
|
4
|
+
|
|
5
|
+
`manifest.json` is the copy mechanism for agent-facing files authored by the P3 content worker. It is a JSON array of entries:
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"src": "claude/skills/morphisms/SKILL.md",
|
|
10
|
+
"dest": "{repo}/.claude/skills/morphisms/SKILL.md",
|
|
11
|
+
"when": "claude"
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- `src` is a relative path under `templates/`.
|
|
16
|
+
- `dest` is an absolute destination after replacing `{repo}` with the initialized repository root.
|
|
17
|
+
- `when` is `claude`, `agent`, or `always` and is filtered by the wizard's flavor answers.
|
|
18
|
+
- The starter contract is not listed here; `init` copies `templates/contract/` directly into `{repo}/.morphisms-contract/issues_front_half/`.
|
|
19
|
+
- Build releases populate `templates/contract/` from `contracts/starter/issues_front_half/`.
|
|
20
|
+
|
|
21
|
+
P3 should append real skill/instruction/onboarding entries to `manifest.json` and add the corresponding files under this directory.
|
|
File without changes
|