popilot 0.2.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/README.md +372 -0
- package/adapters/claude-code/.claude/commands/_domain.md.hbs +32 -0
- package/adapters/claude-code/.claude/commands/analytics.md.hbs +55 -0
- package/adapters/claude-code/.claude/commands/daily.md.hbs +301 -0
- package/adapters/claude-code/.claude/commands/dev.md.hbs +62 -0
- package/adapters/claude-code/.claude/commands/handoff.md +258 -0
- package/adapters/claude-code/.claude/commands/market.md +120 -0
- package/adapters/claude-code/.claude/commands/metrics.md +123 -0
- package/adapters/claude-code/.claude/commands/oscar-loop.md +436 -0
- package/adapters/claude-code/.claude/commands/party.md +85 -0
- package/adapters/claude-code/.claude/commands/plan.md +43 -0
- package/adapters/claude-code/.claude/commands/research.md +203 -0
- package/adapters/claude-code/.claude/commands/retro.md +68 -0
- package/adapters/claude-code/.claude/commands/save.md +440 -0
- package/adapters/claude-code/.claude/commands/sessions.md +139 -0
- package/adapters/claude-code/.claude/commands/sprint.md +106 -0
- package/adapters/claude-code/.claude/commands/start.md +368 -0
- package/adapters/claude-code/.claude/commands/strategy.md +41 -0
- package/adapters/claude-code/.claude/commands/task.md +220 -0
- package/adapters/claude-code/.claude/commands/tracking.md +116 -0
- package/adapters/claude-code/.claude/commands/validate.md +58 -0
- package/adapters/claude-code/CLAUDE.md.hbs +208 -0
- package/adapters/claude-code/manifest.yaml +36 -0
- package/bin/cli.mjs +218 -0
- package/lib/adapter.mjs +68 -0
- package/lib/doctor.mjs +161 -0
- package/lib/hydrate.mjs +421 -0
- package/lib/prompt.mjs +78 -0
- package/lib/scaffold.mjs +155 -0
- package/lib/setup-wizard.mjs +331 -0
- package/lib/template-engine.mjs +164 -0
- package/lib/yaml-lite.mjs +476 -0
- package/package.json +30 -0
- package/scaffold/.context/.secrets.yaml.example +20 -0
- package/scaffold/.context/WORKFLOW.md.hbs +332 -0
- package/scaffold/.context/agents/TEMPLATE.md +115 -0
- package/scaffold/.context/agents/analyst.md.hbs +362 -0
- package/scaffold/.context/agents/developer.md.hbs +390 -0
- package/scaffold/.context/agents/handoff-specialist.md.hbs +292 -0
- package/scaffold/.context/agents/market-researcher.md.hbs +288 -0
- package/scaffold/.context/agents/ollie.md +323 -0
- package/scaffold/.context/agents/operations.md.hbs +293 -0
- package/scaffold/.context/agents/orchestrator.md.hbs +434 -0
- package/scaffold/.context/agents/planner.md.hbs +405 -0
- package/scaffold/.context/agents/qa.md.hbs +409 -0
- package/scaffold/.context/agents/researcher.md.hbs +330 -0
- package/scaffold/.context/agents/sage.md +349 -0
- package/scaffold/.context/agents/strategist.md.hbs +339 -0
- package/scaffold/.context/agents/tracking-governor.md.hbs +291 -0
- package/scaffold/.context/agents/validator.md.hbs +365 -0
- package/scaffold/.context/integrations/_registry.yaml +38 -0
- package/scaffold/.context/integrations/providers/channel_io.yaml +38 -0
- package/scaffold/.context/integrations/providers/corti.yaml +203 -0
- package/scaffold/.context/integrations/providers/ga4.yaml +116 -0
- package/scaffold/.context/integrations/providers/intercom.yaml +47 -0
- package/scaffold/.context/integrations/providers/linear.yaml +46 -0
- package/scaffold/.context/integrations/providers/mixpanel.yaml +73 -0
- package/scaffold/.context/integrations/providers/notebooklm.yaml +74 -0
- package/scaffold/.context/integrations/providers/notion.yaml +129 -0
- package/scaffold/.context/integrations/providers/prod_db.yaml +183 -0
- package/scaffold/.context/oscar/workflows/multi-agent.md +82 -0
- package/scaffold/.context/oscar/workflows/ollie-sage.md +128 -0
- package/scaffold/.context/oscar/workflows/session-git.md +71 -0
- package/scaffold/.context/oscar/workflows/setup.md +663 -0
- package/scaffold/.context/oscar/workflows/tracking.md +118 -0
- package/scaffold/.context/project.yaml.example +102 -0
- package/scaffold/.context/templates/dev-guide.md +217 -0
- package/scaffold/.context/templates/epic-spec.md +225 -0
- package/scaffold/.context/templates/guardrail.md +94 -0
- package/scaffold/.context/templates/handoff-checklist.md +197 -0
- package/scaffold/.context/templates/prd.md +80 -0
- package/scaffold/.context/templates/retrospective.md +78 -0
- package/scaffold/.context/templates/screen-spec.md +714 -0
- package/scaffold/.context/templates/sprint-plan.md +72 -0
- package/scaffold/.context/templates/sprint-status.yaml +109 -0
- package/scaffold/.context/templates/story-v2.md +228 -0
- package/scaffold/.context/templates/validation-report.md +99 -0
- package/scaffold/.gitignore.append +7 -0
- package/scaffold/spec-site/env.d.ts +7 -0
- package/scaffold/spec-site/index.html +14 -0
- package/scaffold/spec-site/package.json +20 -0
- package/scaffold/spec-site/src/App.vue +27 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_ads.svg +10 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_ads_on.svg +10 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_board.svg +14 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_board_on.svg +14 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard.svg +21 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard_on.svg +21 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_pricing.svg +20 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_pricing_on.svg +20 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_store.svg +11 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_store_on.svg +11 -0
- package/scaffold/spec-site/src/components/Accordion.vue +108 -0
- package/scaffold/spec-site/src/components/AppHeader.vue +304 -0
- package/scaffold/spec-site/src/components/Badge.vue +25 -0
- package/scaffold/spec-site/src/components/CoachingCard.vue +112 -0
- package/scaffold/spec-site/src/components/MemoSidebar.vue +239 -0
- package/scaffold/spec-site/src/components/MockupShell.vue +100 -0
- package/scaffold/spec-site/src/components/RuleTable.vue +99 -0
- package/scaffold/spec-site/src/components/ScenarioSwitcher.vue +103 -0
- package/scaffold/spec-site/src/components/SpecNav.vue +26 -0
- package/scaffold/spec-site/src/components/SpecSection.vue +59 -0
- package/scaffold/spec-site/src/components/SummaryGrid.vue +39 -0
- package/scaffold/spec-site/src/components/VersionBadge.vue +38 -0
- package/scaffold/spec-site/src/composables/useActiveSection.ts +53 -0
- package/scaffold/spec-site/src/composables/useMemo.ts +138 -0
- package/scaffold/spec-site/src/composables/useRetro.ts +313 -0
- package/scaffold/spec-site/src/composables/useScenario.ts +43 -0
- package/scaffold/spec-site/src/composables/useScenarioStore.ts +102 -0
- package/scaffold/spec-site/src/composables/useTurso.ts +160 -0
- package/scaffold/spec-site/src/composables/useUser.ts +25 -0
- package/scaffold/spec-site/src/data/navigation.ts +59 -0
- package/scaffold/spec-site/src/data/types.ts +90 -0
- package/scaffold/spec-site/src/data/wireframeRegistry.ts +25 -0
- package/scaffold/spec-site/src/layouts/SplitPaneLayout.vue +79 -0
- package/scaffold/spec-site/src/main.ts +10 -0
- package/scaffold/spec-site/src/pages/IndexPage.vue +66 -0
- package/scaffold/spec-site/src/pages/PolicyDetail.vue +215 -0
- package/scaffold/spec-site/src/pages/PolicyIndex.vue +74 -0
- package/scaffold/spec-site/src/pages/retro/RetroActions.vue +191 -0
- package/scaffold/spec-site/src/pages/retro/RetroBoard.vue +192 -0
- package/scaffold/spec-site/src/pages/retro/RetroCard.vue +131 -0
- package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +287 -0
- package/scaffold/spec-site/src/pages/retro/RetroPage.vue +178 -0
- package/scaffold/spec-site/src/pages/shared/NoContentPlaceholder.vue +34 -0
- package/scaffold/spec-site/src/pages/shared/PlaceholderContent.vue +22 -0
- package/scaffold/spec-site/src/pages/shared/PlaceholderSpecPanel.vue +16 -0
- package/scaffold/spec-site/src/pages/shared/PolicyFallback.vue +145 -0
- package/scaffold/spec-site/src/pages/wireframe/WireframeShell.vue +151 -0
- package/scaffold/spec-site/src/router.ts +85 -0
- package/scaffold/spec-site/src/styles/base.css +21 -0
- package/scaffold/spec-site/src/styles/split-pane.css +143 -0
- package/scaffold/spec-site/src/styles/variables.css +47 -0
- package/scaffold/spec-site/src/utils/markdown.ts +197 -0
- package/scaffold/spec-site/tsconfig.json +20 -0
- package/scaffold/spec-site/vite.config.ts +18 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Handlebars-like template engine
|
|
3
|
+
* Supports: {{var}}, {{nested.var}}, {{#if}}, {{#each}}, {{#unless}}, {{else}}
|
|
4
|
+
* Handles code fences and inline code containing template-like syntax.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a dotted path like "integrations.ga4.property_id" from context object
|
|
9
|
+
*/
|
|
10
|
+
function resolve(ctx, path) {
|
|
11
|
+
return path.split('.').reduce((obj, key) => obj?.[key], ctx);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ── Code fence protection ──────────────────────────────
|
|
15
|
+
// Temporarily replace {{ }} tokens inside code fences and inline code
|
|
16
|
+
// so they aren't matched by block regexes.
|
|
17
|
+
|
|
18
|
+
const PLACEHOLDER_PREFIX = '\x00HBS_';
|
|
19
|
+
|
|
20
|
+
function protect(template) {
|
|
21
|
+
const map = new Map();
|
|
22
|
+
let counter = 0;
|
|
23
|
+
|
|
24
|
+
// Protect fenced code blocks: ```...```
|
|
25
|
+
let result = template.replace(/```[\s\S]*?```/g, (match) => {
|
|
26
|
+
return match.replace(/\{\{[\s\S]*?\}\}/g, (token) => {
|
|
27
|
+
const key = `${PLACEHOLDER_PREFIX}${counter++}\x00`;
|
|
28
|
+
map.set(key, token);
|
|
29
|
+
return key;
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Protect inline code: `...`
|
|
34
|
+
result = result.replace(/`[^`\n]+`/g, (match) => {
|
|
35
|
+
return match.replace(/\{\{[\s\S]*?\}\}/g, (token) => {
|
|
36
|
+
const key = `${PLACEHOLDER_PREFIX}${counter++}\x00`;
|
|
37
|
+
map.set(key, token);
|
|
38
|
+
return key;
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return { text: result, map };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function restore(str, map) {
|
|
46
|
+
let result = str;
|
|
47
|
+
for (const [key, original] of map) {
|
|
48
|
+
result = result.replaceAll(key, original);
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Main render ────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Process a template string with the given context
|
|
57
|
+
* @param {string} template - Template string with {{var}}, {{#if}}, {{#each}} blocks
|
|
58
|
+
* @param {object} ctx - Context object for variable resolution
|
|
59
|
+
* @returns {string} Processed string
|
|
60
|
+
*/
|
|
61
|
+
export function render(template, ctx) {
|
|
62
|
+
// Protect code-fence contents from block matching
|
|
63
|
+
const { text: protected_, map } = protect(template);
|
|
64
|
+
|
|
65
|
+
// Process {{#each}}, {{#if}}, {{#unless}} blocks
|
|
66
|
+
let result = processBlocks(protected_, ctx);
|
|
67
|
+
|
|
68
|
+
// Process simple {{var}} replacements
|
|
69
|
+
result = result.replace(/\{\{([a-zA-Z0-9_.]+)\}\}/g, (_, path) => {
|
|
70
|
+
const val = resolve(ctx, path);
|
|
71
|
+
return val != null ? String(val) : '';
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Restore protected tokens
|
|
75
|
+
result = restore(result, map);
|
|
76
|
+
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Internal render for block bodies — does NOT protect/restore again
|
|
82
|
+
* since the outer render() already did it.
|
|
83
|
+
*/
|
|
84
|
+
function renderInner(template, ctx) {
|
|
85
|
+
let result = processBlocks(template, ctx);
|
|
86
|
+
|
|
87
|
+
result = result.replace(/\{\{([a-zA-Z0-9_.]+)\}\}/g, (_, path) => {
|
|
88
|
+
const val = resolve(ctx, path);
|
|
89
|
+
return val != null ? String(val) : '';
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function processBlocks(str, ctx) {
|
|
96
|
+
let result = str;
|
|
97
|
+
let changed = true;
|
|
98
|
+
|
|
99
|
+
while (changed) {
|
|
100
|
+
changed = false;
|
|
101
|
+
|
|
102
|
+
// {{#each path}}...{{/each}}
|
|
103
|
+
result = result.replace(
|
|
104
|
+
/\{\{#each\s+([a-zA-Z0-9_.]+)\}\}([\s\S]*?)\{\{\/each\}\}/,
|
|
105
|
+
(_, path, body) => {
|
|
106
|
+
changed = true;
|
|
107
|
+
const arr = resolve(ctx, path);
|
|
108
|
+
if (!Array.isArray(arr) || arr.length === 0) return '';
|
|
109
|
+
|
|
110
|
+
// Trim exactly one leading and one trailing newline from body
|
|
111
|
+
// so that template formatting newlines don't accumulate per item
|
|
112
|
+
const trimmedBody = body.replace(/^\n/, '').replace(/\n$/, '');
|
|
113
|
+
|
|
114
|
+
return arr.map((item, index) => {
|
|
115
|
+
const itemCtx = typeof item === 'object'
|
|
116
|
+
? { ...ctx, ...item, '@index': index, '@first': index === 0, '@last': index === arr.length - 1 }
|
|
117
|
+
: { ...ctx, this: item, '@index': index, '@first': index === 0, '@last': index === arr.length - 1 };
|
|
118
|
+
return renderInner(trimmedBody, itemCtx);
|
|
119
|
+
}).join('\n');
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// {{#if path}}...{{else}}...{{/if}}
|
|
124
|
+
result = result.replace(
|
|
125
|
+
/\{\{#if\s+([a-zA-Z0-9_.]+)\}\}([\s\S]*?)\{\{else\}\}([\s\S]*?)\{\{\/if\}\}/,
|
|
126
|
+
(_, path, ifBody, elseBody) => {
|
|
127
|
+
changed = true;
|
|
128
|
+
const val = resolve(ctx, path);
|
|
129
|
+
return isTruthy(val) ? renderInner(ifBody, ctx) : renderInner(elseBody, ctx);
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// {{#if path}}...{{/if}} (no else)
|
|
134
|
+
result = result.replace(
|
|
135
|
+
/\{\{#if\s+([a-zA-Z0-9_.]+)\}\}([\s\S]*?)\{\{\/if\}\}/,
|
|
136
|
+
(_, path, body) => {
|
|
137
|
+
changed = true;
|
|
138
|
+
const val = resolve(ctx, path);
|
|
139
|
+
return isTruthy(val) ? renderInner(body, ctx) : '';
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// {{#unless path}}...{{/unless}}
|
|
144
|
+
result = result.replace(
|
|
145
|
+
/\{\{#unless\s+([a-zA-Z0-9_.]+)\}\}([\s\S]*?)\{\{\/unless\}\}/,
|
|
146
|
+
(_, path, body) => {
|
|
147
|
+
changed = true;
|
|
148
|
+
const val = resolve(ctx, path);
|
|
149
|
+
return !isTruthy(val) ? renderInner(body, ctx) : '';
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isTruthy(val) {
|
|
158
|
+
if (val == null) return false;
|
|
159
|
+
if (val === false) return false;
|
|
160
|
+
if (val === '' ) return false;
|
|
161
|
+
if (val === 0) return false;
|
|
162
|
+
if (Array.isArray(val) && val.length === 0) return false;
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal YAML parser — zero dependencies.
|
|
3
|
+
* Supports the subset used by project.yaml, _registry.yaml, and provider YAML files:
|
|
4
|
+
* - Scalars (string, number, boolean, null)
|
|
5
|
+
* - Inline flow sequences: [a, b, c]
|
|
6
|
+
* - Inline flow mappings: { key: val, key2: val2 }
|
|
7
|
+
* - Block sequences (- item)
|
|
8
|
+
* - Nested maps (indented keys)
|
|
9
|
+
* - Multi-line literal blocks (|)
|
|
10
|
+
* - Comments (#)
|
|
11
|
+
* - Quoted strings ("..." and '...')
|
|
12
|
+
*
|
|
13
|
+
* NOT supported: anchors/aliases (&, *), tags (!!), complex flow styles, merge keys (<<)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse a YAML string into a JS object.
|
|
18
|
+
* @param {string} yaml
|
|
19
|
+
* @returns {any}
|
|
20
|
+
*/
|
|
21
|
+
export function parse(yaml) {
|
|
22
|
+
const lines = yaml.split('\n');
|
|
23
|
+
const { value } = parseNode(lines, 0, -1);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Serialize a JS object to YAML string.
|
|
29
|
+
* @param {any} obj
|
|
30
|
+
* @param {number} [indent=0]
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
export function stringify(obj, indent = 0) {
|
|
34
|
+
if (obj == null) return 'null';
|
|
35
|
+
if (typeof obj === 'boolean') return obj ? 'true' : 'false';
|
|
36
|
+
if (typeof obj === 'number') return String(obj);
|
|
37
|
+
if (typeof obj === 'string') return stringifyString(obj, indent);
|
|
38
|
+
if (Array.isArray(obj)) return stringifyArray(obj, indent);
|
|
39
|
+
return stringifyMap(obj, indent);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Parser internals ────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function parseNode(lines, startIdx, parentIndent) {
|
|
45
|
+
if (startIdx >= lines.length) return { value: null, nextIdx: startIdx };
|
|
46
|
+
|
|
47
|
+
// Skip blank lines and comments to find the first meaningful line
|
|
48
|
+
let idx = startIdx;
|
|
49
|
+
while (idx < lines.length) {
|
|
50
|
+
const stripped = lines[idx].replace(/#.*$/, '').trim();
|
|
51
|
+
if (stripped !== '') break;
|
|
52
|
+
idx++;
|
|
53
|
+
}
|
|
54
|
+
if (idx >= lines.length) return { value: null, nextIdx: idx };
|
|
55
|
+
|
|
56
|
+
const line = lines[idx];
|
|
57
|
+
const lineIndent = getIndent(line);
|
|
58
|
+
const content = stripComment(line).trim();
|
|
59
|
+
|
|
60
|
+
// If this line starts a sequence item at the current level
|
|
61
|
+
if (content.startsWith('- ') || content === '-') {
|
|
62
|
+
return parseBlockSequence(lines, idx, lineIndent);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// If this line is a mapping key
|
|
66
|
+
if (content.includes(':')) {
|
|
67
|
+
return parseBlockMapping(lines, idx, lineIndent);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Standalone scalar
|
|
71
|
+
return { value: parseScalar(content), nextIdx: idx + 1 };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseBlockMapping(lines, startIdx, mapIndent) {
|
|
75
|
+
const result = {};
|
|
76
|
+
let idx = startIdx;
|
|
77
|
+
|
|
78
|
+
while (idx < lines.length) {
|
|
79
|
+
// Skip blank lines and comments
|
|
80
|
+
const raw = lines[idx];
|
|
81
|
+
const stripped = stripComment(raw).trim();
|
|
82
|
+
if (stripped === '') { idx++; continue; }
|
|
83
|
+
|
|
84
|
+
const lineIndent = getIndent(raw);
|
|
85
|
+
|
|
86
|
+
// If dedented past our map level, we're done
|
|
87
|
+
if (lineIndent < mapIndent) break;
|
|
88
|
+
// If at a deeper indent and we're not at the start, it's a child — break
|
|
89
|
+
if (lineIndent > mapIndent && idx !== startIdx) break;
|
|
90
|
+
// If at the same indent but not a key, break
|
|
91
|
+
if (lineIndent === mapIndent && !stripped.includes(':') && !stripped.startsWith('#')) break;
|
|
92
|
+
|
|
93
|
+
// Handle comment-only lines
|
|
94
|
+
if (stripped.startsWith('#')) { idx++; continue; }
|
|
95
|
+
|
|
96
|
+
// Extract key:value
|
|
97
|
+
const colonMatch = stripped.match(/^([^\s:][^:]*?|"[^"]*"|'[^']*')\s*:\s*(.*)/);
|
|
98
|
+
if (!colonMatch) { idx++; continue; }
|
|
99
|
+
|
|
100
|
+
let key = colonMatch[1].trim();
|
|
101
|
+
// Unquote key
|
|
102
|
+
if ((key.startsWith('"') && key.endsWith('"')) || (key.startsWith("'") && key.endsWith("'"))) {
|
|
103
|
+
key = key.slice(1, -1);
|
|
104
|
+
}
|
|
105
|
+
const inlineVal = colonMatch[2].trim();
|
|
106
|
+
|
|
107
|
+
if (inlineVal === '|') {
|
|
108
|
+
// Literal block scalar
|
|
109
|
+
const { value, nextIdx } = parseLiteralBlock(lines, idx + 1, mapIndent);
|
|
110
|
+
result[key] = value;
|
|
111
|
+
idx = nextIdx;
|
|
112
|
+
} else if (inlineVal === '>') {
|
|
113
|
+
// Folded block scalar
|
|
114
|
+
const { value, nextIdx } = parseFoldedBlock(lines, idx + 1, mapIndent);
|
|
115
|
+
result[key] = value;
|
|
116
|
+
idx = nextIdx;
|
|
117
|
+
} else if (inlineVal) {
|
|
118
|
+
// Inline value
|
|
119
|
+
result[key] = parseInlineValue(inlineVal);
|
|
120
|
+
idx++;
|
|
121
|
+
} else {
|
|
122
|
+
// Value on next lines (nested map or sequence)
|
|
123
|
+
idx++;
|
|
124
|
+
// Find next non-blank, non-comment line to determine child structure
|
|
125
|
+
let childIdx = idx;
|
|
126
|
+
while (childIdx < lines.length) {
|
|
127
|
+
const cs = stripComment(lines[childIdx]).trim();
|
|
128
|
+
if (cs !== '') break;
|
|
129
|
+
childIdx++;
|
|
130
|
+
}
|
|
131
|
+
if (childIdx >= lines.length || getIndent(lines[childIdx]) <= mapIndent) {
|
|
132
|
+
// No child — empty value
|
|
133
|
+
result[key] = null;
|
|
134
|
+
} else {
|
|
135
|
+
const childIndent = getIndent(lines[childIdx]);
|
|
136
|
+
const childContent = stripComment(lines[childIdx]).trim();
|
|
137
|
+
if (childContent.startsWith('- ') || childContent === '-') {
|
|
138
|
+
const { value, nextIdx } = parseBlockSequence(lines, childIdx, childIndent);
|
|
139
|
+
result[key] = value;
|
|
140
|
+
idx = nextIdx;
|
|
141
|
+
} else {
|
|
142
|
+
const { value, nextIdx } = parseBlockMapping(lines, childIdx, childIndent);
|
|
143
|
+
result[key] = value;
|
|
144
|
+
idx = nextIdx;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { value: result, nextIdx: idx };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function parseBlockSequence(lines, startIdx, seqIndent) {
|
|
154
|
+
const result = [];
|
|
155
|
+
let idx = startIdx;
|
|
156
|
+
|
|
157
|
+
while (idx < lines.length) {
|
|
158
|
+
const raw = lines[idx];
|
|
159
|
+
const stripped = stripComment(raw).trim();
|
|
160
|
+
if (stripped === '') { idx++; continue; }
|
|
161
|
+
|
|
162
|
+
const lineIndent = getIndent(raw);
|
|
163
|
+
if (lineIndent < seqIndent) break;
|
|
164
|
+
if (lineIndent > seqIndent) break; // unexpected deeper indent outside a sequence item
|
|
165
|
+
|
|
166
|
+
if (!stripped.startsWith('-')) break;
|
|
167
|
+
|
|
168
|
+
// Get the content after "- "
|
|
169
|
+
const afterDash = stripped.slice(1).trim();
|
|
170
|
+
|
|
171
|
+
if (afterDash === '' || afterDash === '|') {
|
|
172
|
+
if (afterDash === '|') {
|
|
173
|
+
const { value, nextIdx } = parseLiteralBlock(lines, idx + 1, seqIndent);
|
|
174
|
+
result.push(value);
|
|
175
|
+
idx = nextIdx;
|
|
176
|
+
} else {
|
|
177
|
+
// Block item with nested content on next lines
|
|
178
|
+
idx++;
|
|
179
|
+
let childIdx = idx;
|
|
180
|
+
while (childIdx < lines.length && stripComment(lines[childIdx]).trim() === '') childIdx++;
|
|
181
|
+
if (childIdx < lines.length && getIndent(lines[childIdx]) > seqIndent) {
|
|
182
|
+
const childIndent = getIndent(lines[childIdx]);
|
|
183
|
+
const childContent = stripComment(lines[childIdx]).trim();
|
|
184
|
+
if (childContent.startsWith('- ')) {
|
|
185
|
+
const { value, nextIdx } = parseBlockSequence(lines, childIdx, childIndent);
|
|
186
|
+
result.push(value);
|
|
187
|
+
idx = nextIdx;
|
|
188
|
+
} else {
|
|
189
|
+
const { value, nextIdx } = parseBlockMapping(lines, childIdx, childIndent);
|
|
190
|
+
result.push(value);
|
|
191
|
+
idx = nextIdx;
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
result.push(null);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} else if (afterDash.startsWith('{')) {
|
|
198
|
+
// Inline flow mapping
|
|
199
|
+
result.push(parseInlineValue(afterDash));
|
|
200
|
+
idx++;
|
|
201
|
+
} else if (afterDash.startsWith('[')) {
|
|
202
|
+
result.push(parseInlineValue(afterDash));
|
|
203
|
+
idx++;
|
|
204
|
+
} else if (afterDash.includes(':')) {
|
|
205
|
+
// Mapping starting on the "- " line
|
|
206
|
+
// e.g., "- key: value" — parse as a mapping item
|
|
207
|
+
// The first key-value is on this line, more may follow indented
|
|
208
|
+
const itemIndent = seqIndent + 2; // standard indent after "- "
|
|
209
|
+
// Reconstruct as if the key:value is at itemIndent
|
|
210
|
+
const tempLine = ' '.repeat(itemIndent) + afterDash;
|
|
211
|
+
const tempLines = [tempLine, ...lines.slice(idx + 1)];
|
|
212
|
+
const { value, nextIdx } = parseBlockMapping(tempLines, 0, itemIndent);
|
|
213
|
+
// nextIdx is relative to tempLines; adjust
|
|
214
|
+
idx = idx + 1 + (nextIdx - 1);
|
|
215
|
+
result.push(value);
|
|
216
|
+
} else {
|
|
217
|
+
// Simple scalar item
|
|
218
|
+
result.push(parseScalar(afterDash));
|
|
219
|
+
idx++;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return { value: result, nextIdx: idx };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parseLiteralBlock(lines, startIdx, parentIndent) {
|
|
227
|
+
let idx = startIdx;
|
|
228
|
+
// Determine block indent from first non-empty line
|
|
229
|
+
while (idx < lines.length && lines[idx].trim() === '') idx++;
|
|
230
|
+
if (idx >= lines.length) return { value: '', nextIdx: idx };
|
|
231
|
+
|
|
232
|
+
const blockIndent = getIndent(lines[idx]);
|
|
233
|
+
if (blockIndent <= parentIndent) return { value: '', nextIdx: idx };
|
|
234
|
+
|
|
235
|
+
const collected = [];
|
|
236
|
+
while (idx < lines.length) {
|
|
237
|
+
const raw = lines[idx];
|
|
238
|
+
if (raw.trim() === '') {
|
|
239
|
+
collected.push('');
|
|
240
|
+
idx++;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const li = getIndent(raw);
|
|
244
|
+
if (li < blockIndent) break;
|
|
245
|
+
collected.push(raw.slice(blockIndent));
|
|
246
|
+
idx++;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Trim trailing empty lines
|
|
250
|
+
while (collected.length > 0 && collected[collected.length - 1] === '') collected.pop();
|
|
251
|
+
return { value: collected.join('\n'), nextIdx: idx };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function parseFoldedBlock(lines, startIdx, parentIndent) {
|
|
255
|
+
// Same as literal for our purposes, but join with spaces instead of newlines
|
|
256
|
+
const { value, nextIdx } = parseLiteralBlock(lines, startIdx, parentIndent);
|
|
257
|
+
// For simplicity, keep as-is (folded blocks in our providers use it like literal)
|
|
258
|
+
return { value, nextIdx };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── Inline value parsing ────────────────────────────────
|
|
262
|
+
|
|
263
|
+
function parseInlineValue(str) {
|
|
264
|
+
const s = str.trim();
|
|
265
|
+
if (s.startsWith('[') && s.endsWith(']')) return parseFlowSequence(s);
|
|
266
|
+
if (s.startsWith('{') && s.endsWith('}')) return parseFlowMapping(s);
|
|
267
|
+
return parseScalar(s);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function parseFlowSequence(str) {
|
|
271
|
+
const inner = str.slice(1, -1).trim();
|
|
272
|
+
if (inner === '') return [];
|
|
273
|
+
return splitFlow(inner).map(item => parseInlineValue(item.trim()));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function parseFlowMapping(str) {
|
|
277
|
+
const inner = str.slice(1, -1).trim();
|
|
278
|
+
if (inner === '') return {};
|
|
279
|
+
const result = {};
|
|
280
|
+
const pairs = splitFlow(inner);
|
|
281
|
+
for (const pair of pairs) {
|
|
282
|
+
const colonIdx = pair.indexOf(':');
|
|
283
|
+
if (colonIdx === -1) continue;
|
|
284
|
+
let key = pair.slice(0, colonIdx).trim();
|
|
285
|
+
if ((key.startsWith('"') && key.endsWith('"')) || (key.startsWith("'") && key.endsWith("'"))) {
|
|
286
|
+
key = key.slice(1, -1);
|
|
287
|
+
}
|
|
288
|
+
const val = pair.slice(colonIdx + 1).trim();
|
|
289
|
+
result[key] = parseInlineValue(val);
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Split flow-style items, respecting nested brackets and quotes */
|
|
295
|
+
function splitFlow(str) {
|
|
296
|
+
const items = [];
|
|
297
|
+
let depth = 0;
|
|
298
|
+
let current = '';
|
|
299
|
+
let inQuote = null;
|
|
300
|
+
|
|
301
|
+
for (let i = 0; i < str.length; i++) {
|
|
302
|
+
const ch = str[i];
|
|
303
|
+
if (inQuote) {
|
|
304
|
+
current += ch;
|
|
305
|
+
if (ch === inQuote) inQuote = null;
|
|
306
|
+
} else if (ch === '"' || ch === "'") {
|
|
307
|
+
inQuote = ch;
|
|
308
|
+
current += ch;
|
|
309
|
+
} else if (ch === '[' || ch === '{') {
|
|
310
|
+
depth++;
|
|
311
|
+
current += ch;
|
|
312
|
+
} else if (ch === ']' || ch === '}') {
|
|
313
|
+
depth--;
|
|
314
|
+
current += ch;
|
|
315
|
+
} else if (ch === ',' && depth === 0) {
|
|
316
|
+
items.push(current);
|
|
317
|
+
current = '';
|
|
318
|
+
} else {
|
|
319
|
+
current += ch;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (current.trim()) items.push(current);
|
|
323
|
+
return items;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function parseScalar(str) {
|
|
327
|
+
let s = str.trim();
|
|
328
|
+
|
|
329
|
+
// Handle comments after value
|
|
330
|
+
// But not inside quotes
|
|
331
|
+
if (!s.startsWith('"') && !s.startsWith("'")) {
|
|
332
|
+
const commentIdx = findCommentStart(s);
|
|
333
|
+
if (commentIdx > 0) s = s.slice(0, commentIdx).trim();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Quoted strings
|
|
337
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
338
|
+
return s.slice(1, -1);
|
|
339
|
+
}
|
|
340
|
+
// Boolean
|
|
341
|
+
if (s === 'true') return true;
|
|
342
|
+
if (s === 'false') return false;
|
|
343
|
+
// Null
|
|
344
|
+
if (s === 'null' || s === '~' || s === '') return null;
|
|
345
|
+
// Number
|
|
346
|
+
if (/^-?\d+(\.\d+)?$/.test(s)) return Number(s);
|
|
347
|
+
return s;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function findCommentStart(str) {
|
|
351
|
+
// Find # that's preceded by whitespace and not inside quotes
|
|
352
|
+
for (let i = 1; i < str.length; i++) {
|
|
353
|
+
if (str[i] === '#' && /\s/.test(str[i - 1])) return i;
|
|
354
|
+
}
|
|
355
|
+
return -1;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ── Comment stripping ───────────────────────────────────
|
|
359
|
+
|
|
360
|
+
function stripComment(line) {
|
|
361
|
+
// Don't strip inside quotes
|
|
362
|
+
let inQuote = null;
|
|
363
|
+
for (let i = 0; i < line.length; i++) {
|
|
364
|
+
const ch = line[i];
|
|
365
|
+
if (inQuote) {
|
|
366
|
+
if (ch === inQuote) inQuote = null;
|
|
367
|
+
} else if (ch === '"' || ch === "'") {
|
|
368
|
+
inQuote = ch;
|
|
369
|
+
} else if (ch === '#') {
|
|
370
|
+
// Only treat as comment if preceded by whitespace or at start
|
|
371
|
+
if (i === 0 || /\s/.test(line[i - 1])) {
|
|
372
|
+
return line.slice(0, i);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return line;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function getIndent(line) {
|
|
380
|
+
const match = line.match(/^(\s*)/);
|
|
381
|
+
return match ? match[1].length : 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── Serializer internals ────────────────────────────────
|
|
385
|
+
|
|
386
|
+
function stringifyString(str, indent) {
|
|
387
|
+
// Use literal block for multi-line strings
|
|
388
|
+
if (str.includes('\n')) {
|
|
389
|
+
const pad = ' '.repeat(indent + 2);
|
|
390
|
+
return '|\n' + str.split('\n').map(l => pad + l).join('\n');
|
|
391
|
+
}
|
|
392
|
+
// Quote if contains special chars
|
|
393
|
+
if (needsQuoting(str)) return `"${str.replace(/"/g, '\\"')}"`;
|
|
394
|
+
return str;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function needsQuoting(str) {
|
|
398
|
+
if (str === '') return true;
|
|
399
|
+
if (str === 'true' || str === 'false' || str === 'null' || str === '~') return true;
|
|
400
|
+
if (/^-?\d+(\.\d+)?$/.test(str)) return true;
|
|
401
|
+
if (/[:{}\[\],&*?|>!%@`#]/.test(str)) return true;
|
|
402
|
+
if (str.startsWith(' ') || str.endsWith(' ')) return true;
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function stringifyArray(arr, indent) {
|
|
407
|
+
if (arr.length === 0) return '[]';
|
|
408
|
+
// For arrays of simple scalars, use inline flow
|
|
409
|
+
if (arr.every(v => v == null || typeof v !== 'object')) {
|
|
410
|
+
const items = arr.map(v => {
|
|
411
|
+
if (v == null) return 'null';
|
|
412
|
+
if (typeof v === 'string') return needsQuoting(v) ? `"${v}"` : v;
|
|
413
|
+
return String(v);
|
|
414
|
+
});
|
|
415
|
+
return `[${items.join(', ')}]`;
|
|
416
|
+
}
|
|
417
|
+
// Block sequence for complex items
|
|
418
|
+
const pad = ' '.repeat(indent);
|
|
419
|
+
return arr.map(item => {
|
|
420
|
+
if (item == null) return `${pad}- null`;
|
|
421
|
+
if (typeof item !== 'object') {
|
|
422
|
+
return `${pad}- ${stringify(item, indent + 2)}`;
|
|
423
|
+
}
|
|
424
|
+
// Object item: render keys at indent+2 (after "- "), first key on same line as "-"
|
|
425
|
+
const entries = Object.entries(item);
|
|
426
|
+
const lines = entries.map(([key, val]) => {
|
|
427
|
+
const qKey = needsQuoting(key) ? `"${key}"` : key;
|
|
428
|
+
if (val == null) return `${qKey}: null`;
|
|
429
|
+
if (typeof val === 'string' && val.includes('\n')) {
|
|
430
|
+
return `${qKey}: ${stringifyString(val, indent + 2)}`;
|
|
431
|
+
}
|
|
432
|
+
if (typeof val !== 'object') {
|
|
433
|
+
return `${qKey}: ${stringify(val, indent + 4)}`;
|
|
434
|
+
}
|
|
435
|
+
if (Array.isArray(val)) {
|
|
436
|
+
if (val.length === 0) return `${qKey}: []`;
|
|
437
|
+
if (val.every(v => v == null || typeof v !== 'object')) {
|
|
438
|
+
return `${qKey}: ${stringify(val, indent + 4)}`;
|
|
439
|
+
}
|
|
440
|
+
return `${qKey}:\n${stringify(val, indent + 4)}`;
|
|
441
|
+
}
|
|
442
|
+
return `${qKey}:\n${stringify(val, indent + 4)}`;
|
|
443
|
+
});
|
|
444
|
+
const contentPad = ' '.repeat(indent + 2);
|
|
445
|
+
let result = `${pad}- ${lines[0]}`;
|
|
446
|
+
for (let i = 1; i < lines.length; i++) {
|
|
447
|
+
result += `\n${contentPad}${lines[i]}`;
|
|
448
|
+
}
|
|
449
|
+
return result;
|
|
450
|
+
}).join('\n');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function stringifyMap(obj, indent) {
|
|
454
|
+
const pad = ' '.repeat(indent);
|
|
455
|
+
const entries = Object.entries(obj);
|
|
456
|
+
if (entries.length === 0) return '{}';
|
|
457
|
+
return entries.map(([key, val]) => {
|
|
458
|
+
const qKey = needsQuoting(key) ? `"${key}"` : key;
|
|
459
|
+
if (val == null) return `${pad}${qKey}: null`;
|
|
460
|
+
if (typeof val === 'string' && val.includes('\n')) {
|
|
461
|
+
return `${pad}${qKey}: ${stringifyString(val, indent)}`;
|
|
462
|
+
}
|
|
463
|
+
if (typeof val !== 'object') {
|
|
464
|
+
return `${pad}${qKey}: ${stringify(val, indent + 2)}`;
|
|
465
|
+
}
|
|
466
|
+
if (Array.isArray(val)) {
|
|
467
|
+
if (val.length === 0) return `${pad}${qKey}: []`;
|
|
468
|
+
if (val.every(v => v == null || typeof v !== 'object')) {
|
|
469
|
+
return `${pad}${qKey}: ${stringify(val, indent + 2)}`;
|
|
470
|
+
}
|
|
471
|
+
return `${pad}${qKey}:\n${stringify(val, indent + 2)}`;
|
|
472
|
+
}
|
|
473
|
+
// Nested map
|
|
474
|
+
return `${pad}${qKey}:\n${stringify(val, indent + 2)}`;
|
|
475
|
+
}).join('\n');
|
|
476
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "popilot",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Multi-agent PO/PM system scaffold for Claude Code",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"popilot": "./bin/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test test/*.test.mjs"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/",
|
|
14
|
+
"lib/",
|
|
15
|
+
"scaffold/",
|
|
16
|
+
"adapters/",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"claude-code",
|
|
21
|
+
"po",
|
|
22
|
+
"pm",
|
|
23
|
+
"multi-agent",
|
|
24
|
+
"scaffold"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 민감 정보 - 이 파일은 gitignore 대상입니다.
|
|
2
|
+
# 필요한 값을 채워주세요.
|
|
3
|
+
# Setup Wizard가 연동 설정에 따라 이 파일을 안내합니다.
|
|
4
|
+
|
|
5
|
+
# 예시 (프로젝트에 따라 다름)
|
|
6
|
+
INTEGRATIONS:
|
|
7
|
+
# GA4_PROPERTY_ID: ""
|
|
8
|
+
# NOTION_WORKSPACE: ""
|
|
9
|
+
# CHANNEL_IO_ACCESS_KEY: ""
|
|
10
|
+
# CHANNEL_IO_ACCESS_SECRET: ""
|
|
11
|
+
# CORTI_API_KEY: "" # Corti MCP Bearer token (MCP 설정에서 사용)
|
|
12
|
+
|
|
13
|
+
# 노션 태스크 연동 (선택)
|
|
14
|
+
NOTION_TASK:
|
|
15
|
+
# USER_ID: ""
|
|
16
|
+
# SPRINT_ID: ""
|
|
17
|
+
|
|
18
|
+
_META:
|
|
19
|
+
UPDATED_AT: ""
|
|
20
|
+
NOTE: "이 파일은 Git에 커밋되지 않습니다."
|