@donghyeonlee/jjamppong-harness 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +77 -0
- package/CONTEXT.md +51 -0
- package/README.md +85 -0
- package/bin/jjamppong.js +123 -0
- package/handoff.md +13 -0
- package/harness/contracts/capability-catalog.yaml +128 -0
- package/harness/contracts/gate-contract-matrix.yaml +188 -0
- package/harness/contracts/installer-contract.yaml +79 -0
- package/harness/contracts/ledger-event.schema.yaml +79 -0
- package/harness/contracts/path-policy.schema.yaml +51 -0
- package/harness/contracts/permission-decision.schema.yaml +95 -0
- package/harness/contracts/task.schema.yaml +88 -0
- package/harness/docs/adr/.gitkeep +1 -0
- package/harness/docs/adr/2026-06-02-jjamppong-planning-gate.md +33 -0
- package/harness/docs/agents/domain.md +14 -0
- package/harness/docs/agents/issue-tracker.md +9 -0
- package/harness/docs/agents/matt-pocock-skills.md +60 -0
- package/harness/docs/agents/triage-labels.md +11 -0
- package/harness/docs/solutions/.gitkeep +1 -0
- package/harness/docs/tasks/active/.gitkeep +1 -0
- package/harness/docs/tasks/archive/.gitkeep +1 -0
- package/harness/docs/tasks/archive/index.md +5 -0
- package/harness/docs/tasks/index.md +13 -0
- package/harness/doctor/doctor.js +114 -0
- package/harness/installer/install.js +235 -0
- package/harness/lifecycle/lifecycle.js +133 -0
- package/harness/permission/permission-decision.js +377 -0
- package/harness/release/CHECKSUMS.sha256 +84 -0
- package/harness/release/RELEASE-NOTES.md +33 -0
- package/harness/release/SOURCE-MANIFEST.md +31 -0
- package/harness/rules/module-types.md +98 -0
- package/harness/rules/rules.md +220 -0
- package/harness/rules/workflow.md +252 -0
- package/harness/state/compound.md +7 -0
- package/harness/state/intake.md +11 -0
- package/harness/state/module-structure.md +13 -0
- package/harness/state/planning.md +21 -0
- package/harness/templates/module/module-state.md +13 -0
- package/harness/templates/task/archive-summary.md +19 -0
- package/harness/templates/task/events.jsonl.template +1 -0
- package/harness/templates/task/gate-ledger.md +21 -0
- package/harness/templates/task/implementation-approval.md +11 -0
- package/harness/templates/task/planning/00-current-planning-context.md +3 -0
- package/harness/templates/task/planning/01-grill-summary.md +3 -0
- package/harness/templates/task/planning/02-research-summary.md +3 -0
- package/harness/templates/task/planning/02b-compound-lookup.md +3 -0
- package/harness/templates/task/planning/02c-architecture-orientation.md +3 -0
- package/harness/templates/task/planning/03-prd.md +3 -0
- package/harness/templates/task/planning/04-issues.md +3 -0
- package/harness/templates/task/planning/05-module-structure.md +3 -0
- package/harness/templates/task/planning/06-writing-plan.md +3 -0
- package/harness/templates/task/planning/07-plan-review.md +3 -0
- package/harness/templates/task/planning-pack.md +23 -0
- package/harness/templates/task/task.yaml +10 -0
- package/harness/templates/task/verification.md +12 -0
- package/harness/verify/verify.js +271 -0
- package/module-template/MODULE.md +25 -0
- package/module-template/README.md +9 -0
- package/modules/.gitkeep +0 -0
- package/package.json +40 -0
- package/proposals/README.md +16 -0
- package/scripts/install-jjamppong-harness.ps1 +62 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const REQUIRED_ROOT_ITEMS = [
|
|
8
|
+
'AGENTS.md',
|
|
9
|
+
'README.md',
|
|
10
|
+
'CONTEXT.md',
|
|
11
|
+
'handoff.md',
|
|
12
|
+
'harness',
|
|
13
|
+
'modules',
|
|
14
|
+
'module-template',
|
|
15
|
+
'proposals',
|
|
16
|
+
'harness.lock.yaml',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const REQUIRED_CONTRACTS = [
|
|
20
|
+
'harness/contracts/capability-catalog.yaml',
|
|
21
|
+
'harness/contracts/gate-contract-matrix.yaml',
|
|
22
|
+
'harness/contracts/ledger-event.schema.yaml',
|
|
23
|
+
'harness/contracts/permission-decision.schema.yaml',
|
|
24
|
+
'harness/contracts/path-policy.schema.yaml',
|
|
25
|
+
'harness/contracts/task.schema.yaml',
|
|
26
|
+
'harness/contracts/installer-contract.yaml',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const REQUIRED_LOCK_FIELDS = [
|
|
30
|
+
'planning_started',
|
|
31
|
+
'github_repo_created',
|
|
32
|
+
'commit_created',
|
|
33
|
+
'push_performed',
|
|
34
|
+
'managed_files',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function parseArgs(argv) {
|
|
38
|
+
const args = {};
|
|
39
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
40
|
+
const token = argv[i];
|
|
41
|
+
if (!token.startsWith('--')) continue;
|
|
42
|
+
const key = token.slice(2);
|
|
43
|
+
const next = argv[i + 1];
|
|
44
|
+
if (!next || next.startsWith('--')) {
|
|
45
|
+
args[key] = true;
|
|
46
|
+
} else {
|
|
47
|
+
args[key] = next;
|
|
48
|
+
i += 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return args;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readText(filePath) {
|
|
55
|
+
if (!fs.existsSync(filePath)) return '';
|
|
56
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readJsonl(filePath, failures) {
|
|
60
|
+
if (!fs.existsSync(filePath)) return [];
|
|
61
|
+
const lines = readText(filePath).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
62
|
+
const events = [];
|
|
63
|
+
lines.forEach((line, index) => {
|
|
64
|
+
try {
|
|
65
|
+
events.push(JSON.parse(line));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
failures.push({
|
|
68
|
+
id: 'invalid_jsonl',
|
|
69
|
+
severity: 'P0',
|
|
70
|
+
message: `Invalid JSONL in ${filePath}:${index + 1}: ${error.message}`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return events;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function yamlScalar(text, key) {
|
|
78
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
79
|
+
const match = text.match(new RegExp(`^\\s*${escaped}:\\s*(.+?)\\s*$`, 'm'));
|
|
80
|
+
return match ? match[1].replace(/^["']|["']$/g, '') : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function yamlBoolean(text, key) {
|
|
84
|
+
const value = yamlScalar(text, key);
|
|
85
|
+
if (value === null) return null;
|
|
86
|
+
if (/^true$/i.test(value)) return true;
|
|
87
|
+
if (/^false$/i.test(value)) return false;
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function listDirectories(dirPath) {
|
|
92
|
+
if (!fs.existsSync(dirPath)) return [];
|
|
93
|
+
return fs.readdirSync(dirPath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function verifyEventHashChain(events, taskName, failures) {
|
|
97
|
+
for (let i = 0; i < events.length; i += 1) {
|
|
98
|
+
const event = events[i];
|
|
99
|
+
if (!event.event_id || !event.event_type || !event.event_hash) {
|
|
100
|
+
failures.push({
|
|
101
|
+
id: 'event_required_fields',
|
|
102
|
+
severity: 'P0',
|
|
103
|
+
message: `Task ${taskName} has an event missing event_id/event_type/event_hash.`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (i > 0 && event.previous_hash !== events[i - 1].event_hash) {
|
|
107
|
+
failures.push({
|
|
108
|
+
id: 'event_hash_chain_broken',
|
|
109
|
+
severity: 'P0',
|
|
110
|
+
message: `Task ${taskName} has broken event hash chain at event ${event.event_id || i}.`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function hasApprovalDecision(events) {
|
|
117
|
+
return events.some((event) => {
|
|
118
|
+
const payload = event.payload || {};
|
|
119
|
+
return event.event_type === 'approval_decision' && payload.status === 'approved';
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function verifyActiveTasks(root, failures, warnings) {
|
|
124
|
+
const activeDir = path.join(root, 'harness', 'docs', 'tasks', 'active');
|
|
125
|
+
if (!fs.existsSync(activeDir)) return;
|
|
126
|
+
|
|
127
|
+
const tasks = listDirectories(activeDir).filter((name) => name !== '.gitkeep');
|
|
128
|
+
if (tasks.length > 1) {
|
|
129
|
+
failures.push({
|
|
130
|
+
id: 'active_task_single_default',
|
|
131
|
+
severity: 'P0',
|
|
132
|
+
message: `Multiple active tasks found without parallel approval: ${tasks.join(', ')}`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const task of tasks) {
|
|
137
|
+
const taskDir = path.join(activeDir, task);
|
|
138
|
+
const eventsPath = path.join(taskDir, 'events.jsonl');
|
|
139
|
+
const taskYamlPath = path.join(taskDir, 'task.yaml');
|
|
140
|
+
const events = readJsonl(eventsPath, failures);
|
|
141
|
+
verifyEventHashChain(events, task, failures);
|
|
142
|
+
|
|
143
|
+
if (fs.existsSync(taskYamlPath)) {
|
|
144
|
+
const taskYaml = readText(taskYamlPath);
|
|
145
|
+
if (/(approved|allowed|permission|capabilit)/i.test(taskYaml) && !hasApprovalDecision(events)) {
|
|
146
|
+
failures.push({
|
|
147
|
+
id: 'projection_without_canonical_event',
|
|
148
|
+
severity: 'P0',
|
|
149
|
+
message: `Task ${task} has task.yaml permission-like state without approval_decision in events.jsonl.`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
warnings.push({
|
|
154
|
+
id: 'active_task_missing_task_yaml',
|
|
155
|
+
severity: 'warning',
|
|
156
|
+
message: `Task ${task} has no task.yaml derived cache.`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function verifyHarnessLock(root, failures) {
|
|
163
|
+
const lockPath = path.join(root, 'harness.lock.yaml');
|
|
164
|
+
const text = readText(lockPath);
|
|
165
|
+
if (!text) return;
|
|
166
|
+
|
|
167
|
+
for (const field of REQUIRED_LOCK_FIELDS) {
|
|
168
|
+
if (!text.includes(field)) {
|
|
169
|
+
failures.push({
|
|
170
|
+
id: 'harness_lock_missing_field',
|
|
171
|
+
severity: 'P0',
|
|
172
|
+
message: `harness.lock.yaml missing required field: ${field}`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const field of ['planning_started', 'github_repo_created', 'commit_created', 'push_performed']) {
|
|
178
|
+
const value = yamlBoolean(text, field);
|
|
179
|
+
if (value !== false) {
|
|
180
|
+
failures.push({
|
|
181
|
+
id: `${field}_during_install`,
|
|
182
|
+
severity: 'P0',
|
|
183
|
+
message: `harness.lock.yaml must record ${field}: false for install-only safety.`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function verifyRoot(root) {
|
|
190
|
+
const projectRoot = path.resolve(root);
|
|
191
|
+
const failures = [];
|
|
192
|
+
const warnings = [];
|
|
193
|
+
|
|
194
|
+
for (const item of REQUIRED_ROOT_ITEMS) {
|
|
195
|
+
if (!fs.existsSync(path.join(projectRoot, item))) {
|
|
196
|
+
failures.push({
|
|
197
|
+
id: 'missing_root_item',
|
|
198
|
+
severity: 'P0',
|
|
199
|
+
message: `Missing required root item: ${item}`,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const contract of REQUIRED_CONTRACTS) {
|
|
205
|
+
if (!fs.existsSync(path.join(projectRoot, contract))) {
|
|
206
|
+
failures.push({
|
|
207
|
+
id: 'missing_contract',
|
|
208
|
+
severity: 'P0',
|
|
209
|
+
message: `Missing contract file: ${contract}`,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const nested of ['jjamppong-harness', 'ourosuper-harness']) {
|
|
215
|
+
if (fs.existsSync(path.join(projectRoot, nested, 'AGENTS.md'))) {
|
|
216
|
+
failures.push({
|
|
217
|
+
id: 'nested_harness_folder',
|
|
218
|
+
severity: 'P0',
|
|
219
|
+
message: `Forbidden nested harness folder found: ${nested}/`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const secretName of ['.env', '.env.local', 'credentials.json', 'service-account.json', 'cookies.txt', 'token.txt']) {
|
|
225
|
+
if (fs.existsSync(path.join(projectRoot, secretName))) {
|
|
226
|
+
failures.push({
|
|
227
|
+
id: 'secret_file_present',
|
|
228
|
+
severity: 'P0',
|
|
229
|
+
message: `Secret-like file present at root and must not be read or published: ${secretName}`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
verifyHarnessLock(projectRoot, failures);
|
|
235
|
+
verifyActiveTasks(projectRoot, failures, warnings);
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
ok: failures.length === 0,
|
|
239
|
+
root: projectRoot,
|
|
240
|
+
failures,
|
|
241
|
+
warnings,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function main() {
|
|
246
|
+
const args = parseArgs(process.argv.slice(2));
|
|
247
|
+
const root = args.root || process.cwd();
|
|
248
|
+
const result = verifyRoot(root);
|
|
249
|
+
if (args.json) {
|
|
250
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
251
|
+
} else if (result.ok) {
|
|
252
|
+
process.stdout.write(`verify passed for ${result.root}\n`);
|
|
253
|
+
} else {
|
|
254
|
+
process.stdout.write(`verify failed for ${result.root}\n`);
|
|
255
|
+
for (const failure of result.failures) {
|
|
256
|
+
process.stdout.write(`FAIL ${failure.id}: ${failure.message}\n`);
|
|
257
|
+
}
|
|
258
|
+
for (const warning of result.warnings) {
|
|
259
|
+
process.stdout.write(`WARN ${warning.id}: ${warning.message}\n`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (require.main === module) {
|
|
266
|
+
main();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
module.exports = {
|
|
270
|
+
verifyRoot,
|
|
271
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Module Contract
|
|
2
|
+
|
|
3
|
+
This file is copied when a module is created.
|
|
4
|
+
|
|
5
|
+
Before using it in a real module, the module must already be allowed by:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
harness/state/module-structure.md
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Purpose
|
|
12
|
+
|
|
13
|
+
No module-specific purpose is recorded in the template.
|
|
14
|
+
|
|
15
|
+
## Boundaries
|
|
16
|
+
|
|
17
|
+
No module-specific boundaries are recorded in the template.
|
|
18
|
+
|
|
19
|
+
## Allowed Dependencies
|
|
20
|
+
|
|
21
|
+
No module-specific dependencies are recorded in the template.
|
|
22
|
+
|
|
23
|
+
## Verification
|
|
24
|
+
|
|
25
|
+
No module-specific verification commands are recorded in the template.
|
package/modules/.gitkeep
ADDED
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@donghyeonlee/jjamppong-harness",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Jjamppong Harness safety workflow installer and verifier.",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/vibedong/jjamppong-harness.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/vibedong/jjamppong-harness#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/vibedong/jjamppong-harness/issues"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"jjamppong": "bin/jjamppong.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"AGENTS.md",
|
|
24
|
+
"README.md",
|
|
25
|
+
"CONTEXT.md",
|
|
26
|
+
"handoff.md",
|
|
27
|
+
"bin/",
|
|
28
|
+
"harness/",
|
|
29
|
+
"module-template/",
|
|
30
|
+
"modules/",
|
|
31
|
+
"proposals/",
|
|
32
|
+
"scripts/"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"test:contracts": "powershell -NoProfile -ExecutionPolicy Bypass -File tests/contracts/run-all.ps1"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Proposals
|
|
2
|
+
|
|
3
|
+
This folder stores unapproved harness and workflow rule change proposals.
|
|
4
|
+
|
|
5
|
+
Project module structure changes do not live here. They go through `harness/rules/module-types.md` and are recorded in `harness/state/module-structure.md`.
|
|
6
|
+
|
|
7
|
+
Codex reads this folder only when the user asks to create, review, approve, or reflect a proposal.
|
|
8
|
+
|
|
9
|
+
Proposal lifecycle:
|
|
10
|
+
|
|
11
|
+
1. Create proposal after user approval.
|
|
12
|
+
2. Review proposal with the user.
|
|
13
|
+
3. Reflect approved changes into live files such as `README.md`, `harness/rules/workflow.md`, `harness/rules/rules.md`, or `harness/rules/module-types.md`.
|
|
14
|
+
4. Delete the proposal file after reflection.
|
|
15
|
+
|
|
16
|
+
Do not keep approved proposal files as archive.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(Mandatory = $true, Position = 0)]
|
|
3
|
+
[string]$TemplateSource,
|
|
4
|
+
|
|
5
|
+
[Parameter(Mandatory = $true, Position = 1)]
|
|
6
|
+
[string]$TargetPath,
|
|
7
|
+
|
|
8
|
+
[string]$ProjectRepo,
|
|
9
|
+
[string]$Owner,
|
|
10
|
+
[switch]$AllowOverwrite,
|
|
11
|
+
[switch]$SkipGitHubRepo
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
Set-StrictMode -Version Latest
|
|
15
|
+
$ErrorActionPreference = 'Stop'
|
|
16
|
+
|
|
17
|
+
function Normalize-TargetPath {
|
|
18
|
+
param([string]$PathText)
|
|
19
|
+
|
|
20
|
+
$text = $PathText.Trim()
|
|
21
|
+
if ($text -match '^[A-Za-z]:[^\\/].+') {
|
|
22
|
+
$text = $text.Substring(0, 2) + [IO.Path]::DirectorySeparatorChar + $text.Substring(2)
|
|
23
|
+
}
|
|
24
|
+
return [IO.Path]::GetFullPath($text)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
$target = Normalize-TargetPath $TargetPath
|
|
28
|
+
$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
29
|
+
$templateRoot = (Resolve-Path -LiteralPath (Join-Path $scriptRoot '..')).Path
|
|
30
|
+
|
|
31
|
+
if ($TemplateSource -and (Test-Path -LiteralPath $TemplateSource)) {
|
|
32
|
+
$templateRoot = (Resolve-Path -LiteralPath $TemplateSource).Path
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
|
36
|
+
throw 'Node.js is required for the Jjamppong Harness installer.'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (-not $SkipGitHubRepo) {
|
|
40
|
+
Write-Output 'GitHub repo creation is disabled by default. This wrapper will not create remotes, commits, or pushes.'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if ($ProjectRepo) {
|
|
44
|
+
Write-Output "ProjectRepo argument was received but not applied by install-only wrapper: $ProjectRepo"
|
|
45
|
+
}
|
|
46
|
+
if ($Owner) {
|
|
47
|
+
Write-Output "Owner argument was received but not used because GitHub repo creation is disabled: $Owner"
|
|
48
|
+
}
|
|
49
|
+
if ($AllowOverwrite) {
|
|
50
|
+
Write-Output 'Existing managed files are backed up before overwrite; install still stops after verification.'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
$cli = Join-Path $templateRoot 'bin\jjamppong.js'
|
|
54
|
+
if (-not (Test-Path -LiteralPath $cli)) {
|
|
55
|
+
throw "Jjamppong CLI not found at $cli"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Write-Output "Installing Jjamppong Harness into $target"
|
|
59
|
+
& node $cli install --target $target --template $templateRoot
|
|
60
|
+
if ($LASTEXITCODE -ne 0) {
|
|
61
|
+
throw "Jjamppong Harness install failed with exit code $LASTEXITCODE"
|
|
62
|
+
}
|