a3ip 1.0.0__py3-none-any.whl
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.
- a3ip/__init__.py +9 -0
- a3ip/__main__.py +5 -0
- a3ip/bundle.py +185 -0
- a3ip/cli.py +231 -0
- a3ip/new.py +199 -0
- a3ip/validate.py +419 -0
- a3ip-1.0.0.dist-info/METADATA +352 -0
- a3ip-1.0.0.dist-info/RECORD +12 -0
- a3ip-1.0.0.dist-info/WHEEL +5 -0
- a3ip-1.0.0.dist-info/entry_points.txt +2 -0
- a3ip-1.0.0.dist-info/licenses/LICENSE +201 -0
- a3ip-1.0.0.dist-info/top_level.txt +1 -0
a3ip/__init__.py
ADDED
a3ip/__main__.py
ADDED
a3ip/bundle.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A3IP bundle — generates a .a3ip.bundle file from a package directory.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
_BINARY_EXTENSIONS = {
|
|
12
|
+
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".webp",
|
|
13
|
+
".pdf", ".zip", ".tar", ".gz", ".whl", ".exe", ".dll",
|
|
14
|
+
".ttf", ".woff", ".woff2", ".eot",
|
|
15
|
+
}
|
|
16
|
+
_TEXT_EXTENSIONS = {
|
|
17
|
+
".md", ".yaml", ".yml", ".json", ".py", ".ps1", ".sh",
|
|
18
|
+
".txt", ".html", ".js", ".css", ".ts", ".tsx", ".jsx",
|
|
19
|
+
".xml", ".toml", ".ini", ".cfg", ".env", ".bat", ".cmd",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _is_binary(path: Path) -> bool:
|
|
24
|
+
if path.suffix.lower() in _BINARY_EXTENSIONS:
|
|
25
|
+
return True
|
|
26
|
+
if path.suffix.lower() in _TEXT_EXTENSIONS:
|
|
27
|
+
return False
|
|
28
|
+
try:
|
|
29
|
+
path.read_text(encoding="utf-8")
|
|
30
|
+
return False
|
|
31
|
+
except Exception:
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _get_package_name(pkg_dir: Path) -> str:
|
|
36
|
+
name = pkg_dir.name
|
|
37
|
+
if name.endswith(".a3ip"):
|
|
38
|
+
name = name[:-5]
|
|
39
|
+
return name
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_package_version(pkg_dir: Path) -> str:
|
|
43
|
+
manifest = pkg_dir / "manifest.yaml"
|
|
44
|
+
if not manifest.exists():
|
|
45
|
+
return "1.0.0"
|
|
46
|
+
for line in manifest.read_text(encoding="utf-8").splitlines():
|
|
47
|
+
line = line.strip()
|
|
48
|
+
if line.startswith("version:"):
|
|
49
|
+
return line.split(":", 1)[1].strip().strip('"').strip("'")
|
|
50
|
+
return "1.0.0"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _collect_files(pkg_dir: Path) -> list[Path]:
|
|
54
|
+
files = []
|
|
55
|
+
for fpath in sorted(pkg_dir.rglob("*")):
|
|
56
|
+
if not fpath.is_file():
|
|
57
|
+
continue
|
|
58
|
+
parts = fpath.relative_to(pkg_dir).parts
|
|
59
|
+
if any(p.startswith(".") for p in parts):
|
|
60
|
+
continue
|
|
61
|
+
if "__pycache__" in parts:
|
|
62
|
+
continue
|
|
63
|
+
files.append(fpath)
|
|
64
|
+
return files
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _embed_file(lines: list, rel_path: str, fpath: Path) -> None:
|
|
68
|
+
lines.append("=== FILE: " + rel_path + " ===")
|
|
69
|
+
if _is_binary(fpath):
|
|
70
|
+
lines.append("# encoding: base64")
|
|
71
|
+
lines.append(base64.b64encode(fpath.read_bytes()).decode("ascii"))
|
|
72
|
+
else:
|
|
73
|
+
lines.append(fpath.read_text(encoding="utf-8", errors="replace"))
|
|
74
|
+
lines.append("=== END FILE ===")
|
|
75
|
+
lines.append("")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _hash_file(fpath: Path) -> str:
|
|
79
|
+
h = hashlib.sha256()
|
|
80
|
+
h.update(fpath.read_bytes())
|
|
81
|
+
return "sha256:" + h.hexdigest()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _write_source_manifest(pkg_dir: Path, pkg_files: list, name: str, version: str, now: str) -> None:
|
|
85
|
+
files_hashes = {
|
|
86
|
+
str(f.relative_to(pkg_dir)).replace("\\", "/"): _hash_file(f)
|
|
87
|
+
for f in pkg_files
|
|
88
|
+
}
|
|
89
|
+
source = {"package": name, "version": version, "bundled_at": now, "files": files_hashes}
|
|
90
|
+
(pkg_dir / ".a3ip-source.json").write_text(json.dumps(source, indent=2), encoding="utf-8")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def build_bundle(pkg_dir: Path, output_path: Path, spec_path: Path | None = None) -> dict:
|
|
94
|
+
"""Build a .a3ip.bundle file. Returns stats dict."""
|
|
95
|
+
name = _get_package_name(pkg_dir)
|
|
96
|
+
version = _get_package_version(pkg_dir)
|
|
97
|
+
pkg_files = _collect_files(pkg_dir)
|
|
98
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
99
|
+
spec_embedded = spec_path is not None and spec_path.exists()
|
|
100
|
+
total_files = len(pkg_files) + (1 if spec_embedded else 0)
|
|
101
|
+
|
|
102
|
+
lines = [
|
|
103
|
+
"---",
|
|
104
|
+
'a3ip-bundle: "1.1"',
|
|
105
|
+
"package: " + name,
|
|
106
|
+
'version: "' + version + '"',
|
|
107
|
+
"generated: " + now,
|
|
108
|
+
"files: " + str(total_files),
|
|
109
|
+
"spec_embedded: " + ("true" if spec_embedded else "false"),
|
|
110
|
+
"---",
|
|
111
|
+
"",
|
|
112
|
+
"# ================================================================",
|
|
113
|
+
"# A3IP BUNDLE — AI INSTALLATION INSTRUCTIONS",
|
|
114
|
+
"# ================================================================",
|
|
115
|
+
"#",
|
|
116
|
+
"# This file is an A3IP (AI Infrastructure Installation Package) bundle.",
|
|
117
|
+
"# It contains a complete AI workflow you can install into your workspace.",
|
|
118
|
+
"#",
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
if spec_embedded:
|
|
122
|
+
lines += [
|
|
123
|
+
"# IF YOU ARE NOT FAMILIAR WITH A3IP:",
|
|
124
|
+
"# 1. Read the embedded spec first (A3IP-SPEC.md, the first FILE block below).",
|
|
125
|
+
"# 2. Read manifest.yaml to understand what this package contains.",
|
|
126
|
+
"# 3. Follow INSTALL.md to install it.",
|
|
127
|
+
"#",
|
|
128
|
+
"# IF YOU KNOW A3IP: Read manifest.yaml, then follow INSTALL.md.",
|
|
129
|
+
]
|
|
130
|
+
else:
|
|
131
|
+
lines += [
|
|
132
|
+
"# Read manifest.yaml to understand what this package contains.",
|
|
133
|
+
"# Follow INSTALL.md to install it.",
|
|
134
|
+
"#",
|
|
135
|
+
"# Note: This bundle does not include the A3IP spec.",
|
|
136
|
+
"# If you are unfamiliar with A3IP, ask for the spec before proceeding.",
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
lines += ["#", "# ================================================================", ""]
|
|
140
|
+
|
|
141
|
+
if spec_embedded:
|
|
142
|
+
lines.append("=== FILE: A3IP-SPEC.md ===")
|
|
143
|
+
lines.append(spec_path.read_text(encoding="utf-8", errors="replace"))
|
|
144
|
+
lines.append("=== END FILE ===")
|
|
145
|
+
lines.append("")
|
|
146
|
+
|
|
147
|
+
for fpath in pkg_files:
|
|
148
|
+
rel = str(fpath.relative_to(pkg_dir)).replace("\\", "/")
|
|
149
|
+
_embed_file(lines, rel, fpath)
|
|
150
|
+
|
|
151
|
+
output_path.write_text("\n".join(lines), encoding="utf-8")
|
|
152
|
+
_write_source_manifest(pkg_dir, pkg_files, name, version, now)
|
|
153
|
+
|
|
154
|
+
return {"file_count": len(pkg_files), "spec_embedded": spec_embedded, "output": output_path}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def bundle(pkg_dir_str: str, output_path_str: str | None = None, spec_path_str: str | None = None) -> dict:
|
|
158
|
+
"""Public API: bundle a package. Returns stats dict or raises on error."""
|
|
159
|
+
pkg_dir = Path(pkg_dir_str)
|
|
160
|
+
if not pkg_dir.is_dir():
|
|
161
|
+
raise ValueError("Not a directory: " + pkg_dir_str)
|
|
162
|
+
|
|
163
|
+
spec_path = Path(spec_path_str) if spec_path_str else None
|
|
164
|
+
if spec_path and not spec_path.exists():
|
|
165
|
+
print("Warning: spec file '" + spec_path_str + "' not found — bundling without it.")
|
|
166
|
+
spec_path = None
|
|
167
|
+
|
|
168
|
+
name = _get_package_name(pkg_dir)
|
|
169
|
+
output_path = Path(output_path_str) if output_path_str else pkg_dir.parent / (name + ".a3ip.bundle")
|
|
170
|
+
|
|
171
|
+
mode = "with spec" if spec_path else "without spec"
|
|
172
|
+
print("Building bundle (" + mode + "): " + str(pkg_dir) + " → " + str(output_path))
|
|
173
|
+
|
|
174
|
+
result = build_bundle(pkg_dir, output_path, spec_path)
|
|
175
|
+
size_kb = output_path.stat().st_size / 1024
|
|
176
|
+
|
|
177
|
+
print("Bundle created: " + str(output_path))
|
|
178
|
+
print(" Package files: " + str(result["file_count"]))
|
|
179
|
+
if result["spec_embedded"]:
|
|
180
|
+
print(" Spec embedded: yes (" + spec_path.name + ")")
|
|
181
|
+
else:
|
|
182
|
+
print(" Spec embedded: no (use --spec to include for external distribution)")
|
|
183
|
+
print(" Total size: " + f"{size_kb:.1f} KB")
|
|
184
|
+
|
|
185
|
+
return result
|
a3ip/cli.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""
|
|
2
|
+
a3ip -- CLI for the A3IP package format.
|
|
3
|
+
*Pronounced "ay-trip"*
|
|
4
|
+
|
|
5
|
+
Commands:
|
|
6
|
+
a3ip new <name> Scaffold a new package that passes validate immediately
|
|
7
|
+
a3ip validate <package_dir> Run 9 normative checks; exit 0 if clean
|
|
8
|
+
a3ip bundle <package_dir> Build a .a3ip.bundle file ready for distribution
|
|
9
|
+
a3ip export --format <fmt> Export to another format (cowork-plugin, apm) [planned v0.2]
|
|
10
|
+
|
|
11
|
+
See https://a3ip.dev for the full specification.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from a3ip import __version__, __spec_version__
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _cmd_new(args: argparse.Namespace) -> int:
|
|
21
|
+
from a3ip.new import scaffold
|
|
22
|
+
try:
|
|
23
|
+
pkg_dir = scaffold(args.name, args.output_dir)
|
|
24
|
+
except FileExistsError as e:
|
|
25
|
+
print("Error: " + str(e), file=sys.stderr)
|
|
26
|
+
return 1
|
|
27
|
+
except ValueError as e:
|
|
28
|
+
print("Error: " + str(e), file=sys.stderr)
|
|
29
|
+
return 1
|
|
30
|
+
|
|
31
|
+
print("Created package: " + str(pkg_dir))
|
|
32
|
+
print()
|
|
33
|
+
print(" " + str(pkg_dir / "manifest.yaml"))
|
|
34
|
+
print(" " + str(pkg_dir / "CONFIGURE.md"))
|
|
35
|
+
print(" " + str(pkg_dir / "INSTALL.md"))
|
|
36
|
+
print(" " + str(pkg_dir / "README.md"))
|
|
37
|
+
print(" " + str(pkg_dir / "components" / "skills" / (args.name + ".md")))
|
|
38
|
+
print()
|
|
39
|
+
print("Next steps:")
|
|
40
|
+
print(" 1. Edit manifest.yaml -- fill in description, permissions, components.")
|
|
41
|
+
print(" 2. Edit INSTALL.md -- describe the install steps.")
|
|
42
|
+
print(" 3. Run: a3ip validate " + args.name + "/")
|
|
43
|
+
print(" 4. Run: a3ip bundle " + args.name + "/")
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _cmd_validate(args: argparse.Namespace) -> int:
|
|
48
|
+
from a3ip.validate import validate
|
|
49
|
+
report = validate(args.package_dir)
|
|
50
|
+
if args.json:
|
|
51
|
+
import json
|
|
52
|
+
print(json.dumps(report, indent=2))
|
|
53
|
+
return 0 if report["ok"] else 1
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _cmd_bundle(args: argparse.Namespace) -> int:
|
|
57
|
+
from a3ip.bundle import bundle
|
|
58
|
+
try:
|
|
59
|
+
bundle(
|
|
60
|
+
pkg_dir_str=args.package_dir,
|
|
61
|
+
output_path_str=args.output,
|
|
62
|
+
spec_path_str=args.spec,
|
|
63
|
+
)
|
|
64
|
+
except (ValueError, OSError) as e:
|
|
65
|
+
print("Error: " + str(e), file=sys.stderr)
|
|
66
|
+
return 1
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_EXPORT_FORMATS = {
|
|
71
|
+
"cowork-plugin": (
|
|
72
|
+
"Target: Anthropic Cowork Plugin (.plugin bundle)\n"
|
|
73
|
+
"\n"
|
|
74
|
+
"A3IP packages map cleanly to Cowork plugins: skills become skill files,\n"
|
|
75
|
+
"permissions become connector declarations, configuration becomes plugin settings.\n"
|
|
76
|
+
"A3IP acts as the portability and security layer around the plugin ecosystem --\n"
|
|
77
|
+
"packages authored once can target Cowork, Codex, Cursor, and others.\n"
|
|
78
|
+
"\n"
|
|
79
|
+
"Planned for a3ip v0.2. Track: https://github.com/a3ip-standard/cli/issues"
|
|
80
|
+
),
|
|
81
|
+
"apm": (
|
|
82
|
+
"Target: Microsoft APM (Agent Package Manifest)\n"
|
|
83
|
+
"\n"
|
|
84
|
+
"A3IP is a superset of APM -- every A3IP package can emit a valid APM manifest.\n"
|
|
85
|
+
"The converter preserves components, dependencies, and context declarations.\n"
|
|
86
|
+
"\n"
|
|
87
|
+
"Planned for a3ip v0.2. Track: https://github.com/a3ip-standard/cli/issues"
|
|
88
|
+
),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _cmd_export(args: argparse.Namespace) -> int:
|
|
93
|
+
fmt = args.format
|
|
94
|
+
details = _EXPORT_FORMATS[fmt]
|
|
95
|
+
print("a3ip export --format " + fmt)
|
|
96
|
+
print()
|
|
97
|
+
print(" Status: planned for a3ip v0.2")
|
|
98
|
+
print()
|
|
99
|
+
for line in details.splitlines():
|
|
100
|
+
print((" " + line) if line else "")
|
|
101
|
+
print()
|
|
102
|
+
print(" Until this converter ships, install the package natively:")
|
|
103
|
+
print(" Drop the .a3ip.bundle into any A3IP-compatible AI and ask it to install.")
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
108
|
+
parser = argparse.ArgumentParser(
|
|
109
|
+
prog="a3ip",
|
|
110
|
+
description=(
|
|
111
|
+
"A3IP -- AI Infrastructure Installation Package (pronounced 'ay-trip')\n"
|
|
112
|
+
"Package, validate, and bundle portable AI agent workflows.\n\n"
|
|
113
|
+
"Spec: https://a3ip.dev\n"
|
|
114
|
+
"Docs: https://github.com/a3ip-standard/spec"
|
|
115
|
+
),
|
|
116
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
117
|
+
)
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"--version", action="version",
|
|
120
|
+
version="a3ip " + __version__ + " (spec " + __spec_version__ + ")"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
sub = parser.add_subparsers(dest="command", metavar="<command>")
|
|
124
|
+
sub.required = True
|
|
125
|
+
|
|
126
|
+
# -- new ------------------------------------------------------------------
|
|
127
|
+
p_new = sub.add_parser(
|
|
128
|
+
"new",
|
|
129
|
+
help="Scaffold a new package that passes validate on first run",
|
|
130
|
+
description=(
|
|
131
|
+
"Create a new A3IP package directory with all required files.\n"
|
|
132
|
+
"The scaffolded package passes `a3ip validate` immediately."
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
p_new.add_argument("name", help="Package name (lowercase, hyphens, e.g. my-workflow)")
|
|
136
|
+
p_new.add_argument(
|
|
137
|
+
"--output-dir", metavar="DIR", dest="output_dir", default=None,
|
|
138
|
+
help="Parent directory to create the package in (default: current directory)",
|
|
139
|
+
)
|
|
140
|
+
p_new.set_defaults(func=_cmd_new)
|
|
141
|
+
|
|
142
|
+
# -- validate -------------------------------------------------------------
|
|
143
|
+
p_val = sub.add_parser(
|
|
144
|
+
"validate",
|
|
145
|
+
help="Run 9 normative checks on a package directory",
|
|
146
|
+
description=(
|
|
147
|
+
"Validate an A3IP package against the spec.\n\n"
|
|
148
|
+
"Runs 9 checks:\n"
|
|
149
|
+
" 1. Config coverage -- {{config.*}} refs match manifest declarations\n"
|
|
150
|
+
" 2. Script existence -- declared script files exist on disk\n"
|
|
151
|
+
" 3. Script config reads -- scripts only read declared config keys\n"
|
|
152
|
+
" 4. Cross-platform -- Windows scripts have a cross-platform fallback\n"
|
|
153
|
+
" 5. Auth flows -- auth scripts are referenced in INSTALL.md\n"
|
|
154
|
+
" 6. Changelog present -- CHANGELOG.md required for version > 1.0.0\n"
|
|
155
|
+
" 7. Refresh scripts -- refresh_script keys declared in manifest\n"
|
|
156
|
+
" 8. Trust->permissions -- elevated trust requires a permissions block\n"
|
|
157
|
+
" 9. Trust->plan section -- write/shell trust requires ## Plan in INSTALL.md\n\n"
|
|
158
|
+
"Exit code: 0 if all checks pass, 1 if any errors found.\n"
|
|
159
|
+
"Warnings do not affect the exit code."
|
|
160
|
+
),
|
|
161
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
162
|
+
)
|
|
163
|
+
p_val.add_argument("package_dir", help="Path to the package directory to validate")
|
|
164
|
+
p_val.add_argument(
|
|
165
|
+
"--json", action="store_true",
|
|
166
|
+
help="Print a JSON report in addition to the human-readable output",
|
|
167
|
+
)
|
|
168
|
+
p_val.set_defaults(func=_cmd_validate)
|
|
169
|
+
|
|
170
|
+
# -- bundle ---------------------------------------------------------------
|
|
171
|
+
p_bun = sub.add_parser(
|
|
172
|
+
"bundle",
|
|
173
|
+
help="Build a .a3ip.bundle file ready for distribution",
|
|
174
|
+
description=(
|
|
175
|
+
"Bundle a package directory into a single .a3ip.bundle file.\n\n"
|
|
176
|
+
"The bundle contains every file in the package in a plain-text format\n"
|
|
177
|
+
"that any A3IP-compatible AI can read and install.\n\n"
|
|
178
|
+
"Binary files are base64-encoded. Hidden files and __pycache__ are excluded."
|
|
179
|
+
),
|
|
180
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
181
|
+
)
|
|
182
|
+
p_bun.add_argument("package_dir", help="Path to the package directory to bundle")
|
|
183
|
+
p_bun.add_argument(
|
|
184
|
+
"--output", "-o", metavar="PATH",
|
|
185
|
+
help="Output path for the bundle (default: <package_dir>/../<name>.a3ip.bundle)",
|
|
186
|
+
)
|
|
187
|
+
p_bun.add_argument(
|
|
188
|
+
"--spec", metavar="PATH",
|
|
189
|
+
help=(
|
|
190
|
+
"Embed the A3IP spec file into the bundle. "
|
|
191
|
+
"Use this when distributing to recipients who may not know A3IP."
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
p_bun.set_defaults(func=_cmd_bundle)
|
|
195
|
+
|
|
196
|
+
# -- export ---------------------------------------------------------------
|
|
197
|
+
p_exp = sub.add_parser(
|
|
198
|
+
"export",
|
|
199
|
+
help="Export to another format: cowork-plugin, apm [planned v0.2]",
|
|
200
|
+
description=(
|
|
201
|
+
"Convert an A3IP package to another agent package format.\n\n"
|
|
202
|
+
"A3IP is designed as an interoperability layer -- packages can be exported\n"
|
|
203
|
+
"to platform-native formats without losing permissions or component structure.\n\n"
|
|
204
|
+
"Available formats:\n"
|
|
205
|
+
" cowork-plugin Anthropic Cowork Plugin bundle\n"
|
|
206
|
+
" apm Microsoft Agent Package Manifest\n\n"
|
|
207
|
+
"These commands are documented now so tooling and CI scripts can reference them.\n"
|
|
208
|
+
"They will print a roadmap notice until the converters ship in v0.2."
|
|
209
|
+
),
|
|
210
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
211
|
+
)
|
|
212
|
+
p_exp.add_argument("package_dir", help="Path to the package directory to export")
|
|
213
|
+
p_exp.add_argument(
|
|
214
|
+
"--format", "-f", required=True,
|
|
215
|
+
choices=list(_EXPORT_FORMATS.keys()),
|
|
216
|
+
metavar="FORMAT",
|
|
217
|
+
help="Target format: " + ", ".join(_EXPORT_FORMATS.keys()),
|
|
218
|
+
)
|
|
219
|
+
p_exp.set_defaults(func=_cmd_export)
|
|
220
|
+
|
|
221
|
+
return parser
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def main() -> None:
|
|
225
|
+
parser = _build_parser()
|
|
226
|
+
args = parser.parse_args()
|
|
227
|
+
sys.exit(args.func(args))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
if __name__ == "__main__":
|
|
231
|
+
main()
|
a3ip/new.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A3IP new — scaffold a minimal valid A3IP package.
|
|
3
|
+
|
|
4
|
+
Creates a directory at <name>/ containing all required files.
|
|
5
|
+
The scaffolded package passes `a3ip validate` on the first run.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
_MANIFEST_TEMPLATE = """\
|
|
12
|
+
$schema: https://a3ip.dev/schema/v1.5/manifest.json
|
|
13
|
+
|
|
14
|
+
name: {name}
|
|
15
|
+
version: "1.0.0"
|
|
16
|
+
description: "Describe what this workflow does."
|
|
17
|
+
min_a3ip_spec: "1.5"
|
|
18
|
+
trust_level: read-only
|
|
19
|
+
|
|
20
|
+
# Declare every external resource this package uses.
|
|
21
|
+
# Remove sections that don't apply.
|
|
22
|
+
#
|
|
23
|
+
# permissions:
|
|
24
|
+
# filesystem:
|
|
25
|
+
# - path: "./output/"
|
|
26
|
+
# access: write
|
|
27
|
+
# reason: "Stores generated files."
|
|
28
|
+
# network:
|
|
29
|
+
# - domain: "api.example.com"
|
|
30
|
+
# reason: "Fetches data from the API."
|
|
31
|
+
# mcp:
|
|
32
|
+
# - name: example-mcp
|
|
33
|
+
# reason: "Reads and writes to the connected service."
|
|
34
|
+
|
|
35
|
+
components:
|
|
36
|
+
skills:
|
|
37
|
+
- path: components/skills/{name}.md
|
|
38
|
+
# artifacts:
|
|
39
|
+
# - path: components/artifacts/output.md
|
|
40
|
+
# prompts:
|
|
41
|
+
# - path: components/prompts/main.md
|
|
42
|
+
# scripts:
|
|
43
|
+
# - key: run
|
|
44
|
+
# implementations:
|
|
45
|
+
# - file: scripts/run.py
|
|
46
|
+
# platform: any
|
|
47
|
+
# trust_level: read-only
|
|
48
|
+
|
|
49
|
+
# configuration:
|
|
50
|
+
# - key: api_token
|
|
51
|
+
# label: "API Token"
|
|
52
|
+
# type: string
|
|
53
|
+
# sensitive: true
|
|
54
|
+
# storage: env
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
_CONFIGURE_TEMPLATE = """\
|
|
58
|
+
# CONFIGURE.md — {name}
|
|
59
|
+
# Ask the user these questions before starting the install.
|
|
60
|
+
# Answers fill in {{{{config.*}}}} placeholders in INSTALL.md.
|
|
61
|
+
# Remove this file entirely if the package needs no configuration.
|
|
62
|
+
|
|
63
|
+
## Questions
|
|
64
|
+
|
|
65
|
+
**1. (Example) Which workspace should the workflow use?**
|
|
66
|
+
- Prompt: "Enter the path to your workspace directory:"
|
|
67
|
+
- Key: `workspace_dir`
|
|
68
|
+
- Type: directory_path
|
|
69
|
+
- Default: `./workspace`
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
_INSTALL_TEMPLATE = """\
|
|
73
|
+
# INSTALL.md — {name}
|
|
74
|
+
|
|
75
|
+
## Overview
|
|
76
|
+
|
|
77
|
+
Brief description of what gets installed and what the user will be able to do.
|
|
78
|
+
|
|
79
|
+
## Pre-flight checklist
|
|
80
|
+
|
|
81
|
+
- [ ] (List any requirements the user must have ready before starting)
|
|
82
|
+
|
|
83
|
+
## Steps
|
|
84
|
+
|
|
85
|
+
### Step 1: (First action)
|
|
86
|
+
|
|
87
|
+
Describe what to do here. Reference config values like {{{{config.workspace_dir}}}}.
|
|
88
|
+
|
|
89
|
+
- [ ] Task A
|
|
90
|
+
- [ ] Task B
|
|
91
|
+
|
|
92
|
+
### Step 2: Verify
|
|
93
|
+
|
|
94
|
+
- [ ] Confirm the workflow is working as expected.
|
|
95
|
+
|
|
96
|
+
## Done
|
|
97
|
+
|
|
98
|
+
The `{name}` workflow is now installed.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
_SKILL_TEMPLATE = """\
|
|
102
|
+
# {name}
|
|
103
|
+
|
|
104
|
+
## Overview
|
|
105
|
+
|
|
106
|
+
Describe what this skill does and when to invoke it.
|
|
107
|
+
|
|
108
|
+
## Usage
|
|
109
|
+
|
|
110
|
+
Explain how to use the skill, what inputs it needs, and what output to expect.
|
|
111
|
+
|
|
112
|
+
## Steps
|
|
113
|
+
|
|
114
|
+
1. (First step)
|
|
115
|
+
2. (Second step)
|
|
116
|
+
3. Confirm the result.
|
|
117
|
+
|
|
118
|
+
## Notes
|
|
119
|
+
|
|
120
|
+
Any caveats, limitations, or tips.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
_README_TEMPLATE = """\
|
|
124
|
+
# {name}
|
|
125
|
+
|
|
126
|
+
> One-line description of what this workflow does.
|
|
127
|
+
|
|
128
|
+
## What it does
|
|
129
|
+
|
|
130
|
+
Describe the workflow in 2–3 sentences. What problem does it solve?
|
|
131
|
+
|
|
132
|
+
## Requirements
|
|
133
|
+
|
|
134
|
+
- (List any tools, accounts, or credentials needed)
|
|
135
|
+
|
|
136
|
+
## Quick start
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
a3ip bundle {name}/
|
|
140
|
+
# Drop the resulting .a3ip.bundle file into a conversation with an A3IP-compatible AI.
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Configuration
|
|
144
|
+
|
|
145
|
+
| Key | Description | Required |
|
|
146
|
+
|-----|-------------|----------|
|
|
147
|
+
| (example) `api_token` | API token for the connected service | Yes |
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
[Apache 2.0](LICENSE) — or replace with your chosen license.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def scaffold(name: str, output_dir: str | None = None) -> Path:
|
|
156
|
+
"""
|
|
157
|
+
Create a new A3IP package directory at output_dir/name (or ./name).
|
|
158
|
+
Returns the path to the created directory.
|
|
159
|
+
Raises FileExistsError if the directory already exists.
|
|
160
|
+
"""
|
|
161
|
+
base = Path(output_dir) if output_dir else Path(".")
|
|
162
|
+
pkg_dir = base / name
|
|
163
|
+
|
|
164
|
+
if pkg_dir.exists():
|
|
165
|
+
raise FileExistsError("Directory already exists: " + str(pkg_dir))
|
|
166
|
+
|
|
167
|
+
# Validate name (must match manifest.yaml name pattern)
|
|
168
|
+
import re
|
|
169
|
+
if not re.match(r'^[a-z0-9][a-z0-9-]*[a-z0-9]$', name):
|
|
170
|
+
raise ValueError(
|
|
171
|
+
"Package name must be lowercase alphanumeric with hyphens "
|
|
172
|
+
"(e.g. 'my-workflow'), got: " + repr(name)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Create directory tree
|
|
176
|
+
(pkg_dir / "components" / "skills").mkdir(parents=True)
|
|
177
|
+
(pkg_dir / "scripts").mkdir()
|
|
178
|
+
|
|
179
|
+
# Write files
|
|
180
|
+
(pkg_dir / "manifest.yaml").write_text(
|
|
181
|
+
_MANIFEST_TEMPLATE.format(name=name), encoding="utf-8"
|
|
182
|
+
)
|
|
183
|
+
(pkg_dir / "CONFIGURE.md").write_text(
|
|
184
|
+
_CONFIGURE_TEMPLATE.format(name=name), encoding="utf-8"
|
|
185
|
+
)
|
|
186
|
+
(pkg_dir / "INSTALL.md").write_text(
|
|
187
|
+
_INSTALL_TEMPLATE.format(name=name), encoding="utf-8"
|
|
188
|
+
)
|
|
189
|
+
(pkg_dir / "README.md").write_text(
|
|
190
|
+
_README_TEMPLATE.format(name=name), encoding="utf-8"
|
|
191
|
+
)
|
|
192
|
+
(pkg_dir / "components" / "skills" / (name + ".md")).write_text(
|
|
193
|
+
_SKILL_TEMPLATE.format(name=name), encoding="utf-8"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Placeholder .gitkeep so scripts/ is committed empty
|
|
197
|
+
(pkg_dir / "scripts" / ".gitkeep").write_text("", encoding="utf-8")
|
|
198
|
+
|
|
199
|
+
return pkg_dir
|